diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index 7aef2827..feff0197 100644 --- a/JellyfinPlayer.xcodeproj/project.pbxproj +++ b/JellyfinPlayer.xcodeproj/project.pbxproj @@ -11,7 +11,6 @@ 53313B90265EEA6D00947AA3 /* VideoPlayer.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 53313B8F265EEA6D00947AA3 /* VideoPlayer.storyboard */; }; 53352571265EA0A0006CCA86 /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = 53352570265EA0A0006CCA86 /* Introspect */; }; 5338F74E263B61370014BF09 /* ConnectToServerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5338F74D263B61370014BF09 /* ConnectToServerView.swift */; }; - 5338F754263B65E10014BF09 /* SwiftyRequest in Frameworks */ = {isa = PBXBuildFile; productRef = 5338F753263B65E10014BF09 /* SwiftyRequest */; }; 5338F757263B7E2E0014BF09 /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 5338F756263B7E2E0014BF09 /* KeychainSwift */; }; 535870632669D21600D05A09 /* JellyfinPlayer_tvOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535870622669D21600D05A09 /* JellyfinPlayer_tvOSApp.swift */; }; 535870652669D21600D05A09 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535870642669D21600D05A09 /* ContentView.swift */; }; @@ -20,14 +19,8 @@ 5358706C2669D21700D05A09 /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5358706B2669D21700D05A09 /* Persistence.swift */; }; 5358706F2669D21700D05A09 /* JellyfinPlayer_tvOS.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 5358706D2669D21700D05A09 /* JellyfinPlayer_tvOS.xcdatamodeld */; }; 5358707E2669D64F00D05A09 /* bitrates.json in Resources */ = {isa = PBXBuildFile; fileRef = AE8C3158265D6F90008AA076 /* bitrates.json */; }; - 5358708B2669D7A800D05A09 /* SwiftyRequest in Frameworks */ = {isa = PBXBuildFile; productRef = 5358708A2669D7A800D05A09 /* SwiftyRequest */; }; 5358708D2669D7A800D05A09 /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 5358708C2669D7A800D05A09 /* KeychainSwift */; }; - 5358708F2669D7A800D05A09 /* SwiftyJSON in Frameworks */ = {isa = PBXBuildFile; productRef = 5358708E2669D7A800D05A09 /* SwiftyJSON */; }; 535870912669D7A800D05A09 /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = 535870902669D7A800D05A09 /* Introspect */; }; - 535870932669D7A800D05A09 /* CombineMoya in Frameworks */ = {isa = PBXBuildFile; productRef = 535870922669D7A800D05A09 /* CombineMoya */; }; - 535870952669D7A800D05A09 /* Moya in Frameworks */ = {isa = PBXBuildFile; productRef = 535870942669D7A800D05A09 /* Moya */; }; - 535870972669D7A800D05A09 /* ReactiveMoya in Frameworks */ = {isa = PBXBuildFile; productRef = 535870962669D7A800D05A09 /* ReactiveMoya */; }; - 535870992669D7A800D05A09 /* RxMoya in Frameworks */ = {isa = PBXBuildFile; productRef = 535870982669D7A800D05A09 /* RxMoya */; }; 5358709B2669D7A800D05A09 /* NukeUI in Frameworks */ = {isa = PBXBuildFile; productRef = 5358709A2669D7A800D05A09 /* NukeUI */; }; 5358709D2669D82900D05A09 /* TVVLCKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5358709C2669D82900D05A09 /* TVVLCKit.framework */; }; 5358709E2669D82900D05A09 /* TVVLCKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 5358709C2669D82900D05A09 /* TVVLCKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -52,11 +45,7 @@ 5389276E263C25100035E14B /* ContinueWatchingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5389276D263C25100035E14B /* ContinueWatchingView.swift */; }; 53892770263C25230035E14B /* NextUpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5389276F263C25230035E14B /* NextUpView.swift */; }; 53892772263C8C6F0035E14B /* LoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53892771263C8C6F0035E14B /* LoadingView.swift */; }; - 5389277A263CBFE70035E14B /* SwiftyJSON in Frameworks */ = {isa = PBXBuildFile; productRef = 53892779263CBFE70035E14B /* SwiftyJSON */; }; 5389277C263CC3DB0035E14B /* BlurHashDecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5389277B263CC3DB0035E14B /* BlurHashDecode.swift */; }; - 53987CA426572C1300E7EA70 /* SeasonItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53987CA326572C1300E7EA70 /* SeasonItemView.swift */; }; - 53987CA626572F0700E7EA70 /* SeriesItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53987CA526572F0700E7EA70 /* SeriesItemView.swift */; }; - 53987CA82657424A00E7EA70 /* EpisodeItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53987CA72657424A00E7EA70 /* EpisodeItemView.swift */; }; 539B2DA5263BA5B8007FF1A4 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 539B2DA4263BA5B8007FF1A4 /* SettingsView.swift */; }; 53A089D0264DA9DA00D57806 /* MovieItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53A089CF264DA9DA00D57806 /* MovieItemView.swift */; }; 53A431BD266B0FF20016769F /* JellyfinAPI in Frameworks */ = {isa = PBXBuildFile; productRef = 53A431BC266B0FF20016769F /* JellyfinAPI */; }; @@ -66,25 +55,16 @@ 53D5E3DD264B47EE00BADDC8 /* MobileVLCKit.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 53D5E3DC264B47EE00BADDC8 /* MobileVLCKit.xcframework */; }; 53D5E3DE264B47EE00BADDC8 /* MobileVLCKit.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 53D5E3DC264B47EE00BADDC8 /* MobileVLCKit.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 53DF641E263D9C0600A7CD1A /* LibraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53DF641D263D9C0600A7CD1A /* LibraryView.swift */; }; - 53E4E647263F6CF100F67C6B /* LibraryFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53E4E646263F6CF100F67C6B /* LibraryFilterView.swift */; }; 53E4E649263F725B00F67C6B /* MultiSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53E4E648263F725B00F67C6B /* MultiSelectorView.swift */; }; 53EE24E6265060780068F029 /* LibrarySearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53EE24E5265060780068F029 /* LibrarySearchView.swift */; }; 53F8377D265FF67C00F456B3 /* VideoPlayerSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53F8377C265FF67C00F456B3 /* VideoPlayerSettingsView.swift */; }; 53FF7F2A263CF3F500585C35 /* LatestMediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53FF7F29263CF3F500585C35 /* LatestMediaView.swift */; }; - 6213388E265F777C00A81A2A /* LibraryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6213388D265F777C00A81A2A /* LibraryViewModel.swift */; }; 62133890265F83A900A81A2A /* LibraryListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6213388F265F83A900A81A2A /* LibraryListView.swift */; }; 621338932660107500A81A2A /* StringExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621338922660107500A81A2A /* StringExtensions.swift */; }; - 62133895266096EF00A81A2A /* LibraryListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62133894266096EF00A81A2A /* LibraryListViewModel.swift */; }; 621338B32660A07800A81A2A /* LazyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621338B22660A07800A81A2A /* LazyView.swift */; }; 621C638026672A30004216EA /* NukeUI in Frameworks */ = {isa = PBXBuildFile; productRef = 621C637F26672A30004216EA /* NukeUI */; }; 621C638226676728004216EA /* NukeExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621C638126676728004216EA /* NukeExtensions.swift */; }; 6225FCCB2663841E00E067F6 /* ParallaxHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6225FCCA2663841E00E067F6 /* ParallaxHeader.swift */; }; - 6273DD43265F4195009C1D0B /* Moya in Frameworks */ = {isa = PBXBuildFile; productRef = 6273DD42265F4195009C1D0B /* Moya */; }; - 6273DD45265F4195009C1D0B /* CombineMoya in Frameworks */ = {isa = PBXBuildFile; productRef = 6273DD44265F4195009C1D0B /* CombineMoya */; }; - 6273DD48265F41B3009C1D0B /* JellyfinAPIOld.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6273DD47265F41B3009C1D0B /* JellyfinAPIOld.swift */; }; - 6273DD4E265F47B2009C1D0B /* LibrarySearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6273DD4D265F47B2009C1D0B /* LibrarySearchViewModel.swift */; }; - AE8C3154265D60BF008AA076 /* SettingsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE8C3153265D60BF008AA076 /* SettingsModel.swift */; }; - AE8C3156265D616A008AA076 /* SettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE8C3155265D616A008AA076 /* SettingsViewModel.swift */; }; AE8C3159265D6F90008AA076 /* bitrates.json in Resources */ = {isa = PBXBuildFile; fileRef = AE8C3158265D6F90008AA076 /* bitrates.json */; }; /* End PBXBuildFile section */ @@ -125,7 +105,6 @@ /* Begin PBXFileReference section */ 53192D5C265AA78A008A4215 /* DeviceProfileBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceProfileBuilder.swift; sourceTree = ""; }; 53313B8F265EEA6D00947AA3 /* VideoPlayer.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = VideoPlayer.storyboard; sourceTree = ""; }; - 5333A68A266BDDC10044FD6B /* JellyfinPlayer.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = JellyfinPlayer.entitlements; sourceTree = ""; }; 5338F74D263B61370014BF09 /* ConnectToServerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectToServerView.swift; sourceTree = ""; }; 535870602669D21600D05A09 /* JellyfinPlayer tvOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "JellyfinPlayer tvOS.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 535870622669D21600D05A09 /* JellyfinPlayer_tvOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinPlayer_tvOSApp.swift; sourceTree = ""; }; @@ -166,17 +145,11 @@ 53EE24E5265060780068F029 /* LibrarySearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibrarySearchView.swift; sourceTree = ""; }; 53F8377C265FF67C00F456B3 /* VideoPlayerSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerSettingsView.swift; sourceTree = ""; }; 53FF7F29263CF3F500585C35 /* LatestMediaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LatestMediaView.swift; sourceTree = ""; }; - 6213388D265F777C00A81A2A /* LibraryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryViewModel.swift; sourceTree = ""; }; 6213388F265F83A900A81A2A /* LibraryListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryListView.swift; sourceTree = ""; }; 621338922660107500A81A2A /* StringExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExtensions.swift; sourceTree = ""; }; - 62133894266096EF00A81A2A /* LibraryListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryListViewModel.swift; sourceTree = ""; }; 621338B22660A07800A81A2A /* LazyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyView.swift; sourceTree = ""; }; 621C638126676728004216EA /* NukeExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NukeExtensions.swift; sourceTree = ""; }; 6225FCCA2663841E00E067F6 /* ParallaxHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParallaxHeader.swift; sourceTree = ""; }; - 6273DD47265F41B3009C1D0B /* JellyfinAPIOld.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinAPIOld.swift; sourceTree = ""; }; - 6273DD4D265F47B2009C1D0B /* LibrarySearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibrarySearchViewModel.swift; sourceTree = ""; }; - AE8C3153265D60BF008AA076 /* SettingsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsModel.swift; sourceTree = ""; }; - AE8C3155265D616A008AA076 /* SettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = ""; }; AE8C3158265D6F90008AA076 /* bitrates.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = bitrates.json; sourceTree = ""; }; /* End PBXFileReference section */ @@ -185,17 +158,11 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 535870972669D7A800D05A09 /* ReactiveMoya in Frameworks */, 53A431BF266B0FFE0016769F /* JellyfinAPI in Frameworks */, - 5358708F2669D7A800D05A09 /* SwiftyJSON in Frameworks */, 535870912669D7A800D05A09 /* Introspect in Frameworks */, - 535870952669D7A800D05A09 /* Moya in Frameworks */, 5358708D2669D7A800D05A09 /* KeychainSwift in Frameworks */, 5358709D2669D82900D05A09 /* TVVLCKit.framework in Frameworks */, - 535870932669D7A800D05A09 /* CombineMoya in Frameworks */, 5358709B2669D7A800D05A09 /* NukeUI in Frameworks */, - 535870992669D7A800D05A09 /* RxMoya in Frameworks */, - 5358708B2669D7A800D05A09 /* SwiftyRequest in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -204,14 +171,10 @@ buildActionMask = 2147483647; files = ( 5338F757263B7E2E0014BF09 /* KeychainSwift in Frameworks */, - 6273DD45265F4195009C1D0B /* CombineMoya in Frameworks */, - 6273DD43265F4195009C1D0B /* Moya in Frameworks */, 53D5E3DD264B47EE00BADDC8 /* MobileVLCKit.xcframework in Frameworks */, - 5338F754263B65E10014BF09 /* SwiftyRequest in Frameworks */, 53352571265EA0A0006CCA86 /* Introspect in Frameworks */, 621C638026672A30004216EA /* NukeUI in Frameworks */, 53A431BD266B0FF20016769F /* JellyfinAPI in Frameworks */, - 5389277A263CBFE70035E14B /* SwiftyJSON in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -244,8 +207,8 @@ isa = PBXGroup; children = ( 621338912660106C00A81A2A /* Extensions */, - 535870AB2669D8D300D05A09 /* Typings */, AE8C3157265D6F5E008AA076 /* Resources */, + 535870AB2669D8D300D05A09 /* Typings */, ); path = Shared; sourceTree = ""; @@ -261,12 +224,12 @@ 5377CBE8263B596A003A4E83 = { isa = PBXGroup; children = ( - 535870752669D60C00D05A09 /* Shared */, 53D5E3DA264B460200BADDC8 /* Cartfile */, + 53D5E3DB264B47EE00BADDC8 /* Frameworks */, 5377CBF3263B596A003A4E83 /* JellyfinPlayer */, 535870612669D21600D05A09 /* JellyfinPlayer tvOS */, 5377CBF2263B596A003A4E83 /* Products */, - 53D5E3DB264B47EE00BADDC8 /* Frameworks */, + 535870752669D60C00D05A09 /* Shared */, ); sourceTree = ""; }; @@ -282,31 +245,31 @@ 5377CBF3263B596A003A4E83 /* JellyfinPlayer */ = { isa = PBXGroup; children = ( - 5333A68A266BDDC10044FD6B /* JellyfinPlayer.entitlements */, - 6273DD46265F419B009C1D0B /* APIs */, - AE8C3152265D607B008AA076 /* ViewModels */, - AE8C3151265D6075008AA076 /* Models */, - AE8C3150265D5FE1008AA076 /* Views */, - 5377CBF4263B596A003A4E83 /* JellyfinPlayerApp.swift */, - 5377CC02263B596B003A4E83 /* Info.plist */, - 5377CBF6263B596A003A4E83 /* ContentView.swift */, 5377CBF8263B596B003A4E83 /* Assets.xcassets */, + 5338F74D263B61370014BF09 /* ConnectToServerView.swift */, + 5377CBF6263B596A003A4E83 /* ContentView.swift */, + 5389276D263C25100035E14B /* ContinueWatchingView.swift */, + 53192D5C265AA78A008A4215 /* DeviceProfileBuilder.swift */, + 53987CA72657424A00E7EA70 /* EpisodeItemView.swift */, + 5377CC02263B596B003A4E83 /* Info.plist */, + 535BAE9E2649E569005FA86D /* ItemView.swift */, + 5377CBF4263B596A003A4E83 /* JellyfinPlayerApp.swift */, + 53FF7F29263CF3F500585C35 /* LatestMediaView.swift */, + 53E4E646263F6CF100F67C6B /* LibraryFilterView.swift */, + 6213388F265F83A900A81A2A /* LibraryListView.swift */, + 53EE24E5265060780068F029 /* LibrarySearchView.swift */, + 53DF641D263D9C0600A7CD1A /* LibraryView.swift */, + 53892771263C8C6F0035E14B /* LoadingView.swift */, + 53A089CF264DA9DA00D57806 /* MovieItemView.swift */, + 5389276F263C25230035E14B /* NextUpView.swift */, 5377CBFD263B596B003A4E83 /* PersistenceController.swift */, 5377CBFA263B596B003A4E83 /* Preview Content */, - 5338F74D263B61370014BF09 /* ConnectToServerView.swift */, - 5389276D263C25100035E14B /* ContinueWatchingView.swift */, - 5389276F263C25230035E14B /* NextUpView.swift */, - 53FF7F29263CF3F500585C35 /* LatestMediaView.swift */, - 53DF641D263D9C0600A7CD1A /* LibraryView.swift */, - 53E4E646263F6CF100F67C6B /* LibraryFilterView.swift */, - 535BAE9E2649E569005FA86D /* ItemView.swift */, - 53A089CF264DA9DA00D57806 /* MovieItemView.swift */, - 53EE24E5265060780068F029 /* LibrarySearchView.swift */, 53987CA326572C1300E7EA70 /* SeasonItemView.swift */, 53987CA526572F0700E7EA70 /* SeriesItemView.swift */, - 53987CA72657424A00E7EA70 /* EpisodeItemView.swift */, - 53192D5C265AA78A008A4215 /* DeviceProfileBuilder.swift */, - 6213388F265F83A900A81A2A /* LibraryListView.swift */, + 539B2DA4263BA5B8007FF1A4 /* SettingsView.swift */, + 535BAEA4264A151C005FA86D /* VideoPlayer.swift */, + 53313B8F265EEA6D00947AA3 /* VideoPlayer.storyboard */, + 53F8377C265FF67C00F456B3 /* VideoPlayerSettingsView.swift */, ); path = JellyfinPlayer; sourceTree = ""; @@ -331,62 +294,23 @@ 621338912660106C00A81A2A /* Extensions */ = { isa = PBXGroup; children = ( - 5389277B263CC3DB0035E14B /* BlurHashDecode.swift */, - 53E4E648263F725B00F67C6B /* MultiSelectorView.swift */, - 621338B22660A07800A81A2A /* LazyView.swift */, - 621338922660107500A81A2A /* StringExtensions.swift */, - 6225FCCA2663841E00E067F6 /* ParallaxHeader.swift */, - 621C638126676728004216EA /* NukeExtensions.swift */, - 53C4404D266C75C70049424C /* HandleAPIRequestCompletion.swift */, 5364F454266CA0DC0026ECBA /* APIExtensions.swift */, + 5389277B263CC3DB0035E14B /* BlurHashDecode.swift */, + 53C4404D266C75C70049424C /* HandleAPIRequestCompletion.swift */, + 621338B22660A07800A81A2A /* LazyView.swift */, + 53E4E648263F725B00F67C6B /* MultiSelectorView.swift */, + 621C638126676728004216EA /* NukeExtensions.swift */, + 6225FCCA2663841E00E067F6 /* ParallaxHeader.swift */, + 621338922660107500A81A2A /* StringExtensions.swift */, ); path = Extensions; sourceTree = ""; }; - 6273DD46265F419B009C1D0B /* APIs */ = { - isa = PBXGroup; - children = ( - 6273DD47265F41B3009C1D0B /* JellyfinAPIOld.swift */, - ); - path = APIs; - sourceTree = ""; - }; - AE8C3150265D5FE1008AA076 /* Views */ = { - isa = PBXGroup; - children = ( - 53892771263C8C6F0035E14B /* LoadingView.swift */, - 535BAEA4264A151C005FA86D /* VideoPlayer.swift */, - 53313B8F265EEA6D00947AA3 /* VideoPlayer.storyboard */, - 53F8377C265FF67C00F456B3 /* VideoPlayerSettingsView.swift */, - 539B2DA4263BA5B8007FF1A4 /* SettingsView.swift */, - ); - path = Views; - sourceTree = ""; - }; - AE8C3151265D6075008AA076 /* Models */ = { - isa = PBXGroup; - children = ( - AE8C3153265D60BF008AA076 /* SettingsModel.swift */, - ); - path = Models; - sourceTree = ""; - }; - AE8C3152265D607B008AA076 /* ViewModels */ = { - isa = PBXGroup; - children = ( - 6273DD4D265F47B2009C1D0B /* LibrarySearchViewModel.swift */, - 6213388D265F777C00A81A2A /* LibraryViewModel.swift */, - 62133894266096EF00A81A2A /* LibraryListViewModel.swift */, - AE8C3155265D616A008AA076 /* SettingsViewModel.swift */, - ); - path = ViewModels; - sourceTree = ""; - }; AE8C3157265D6F5E008AA076 /* Resources */ = { isa = PBXGroup; children = ( - 5377CBFF263B596B003A4E83 /* Model.xcdatamodeld */, AE8C3158265D6F90008AA076 /* bitrates.json */, + 5377CBFF263B596B003A4E83 /* Model.xcdatamodeld */, ); path = Resources; sourceTree = ""; @@ -409,14 +333,8 @@ ); name = "JellyfinPlayer tvOS"; packageProductDependencies = ( - 5358708A2669D7A800D05A09 /* SwiftyRequest */, 5358708C2669D7A800D05A09 /* KeychainSwift */, - 5358708E2669D7A800D05A09 /* SwiftyJSON */, 535870902669D7A800D05A09 /* Introspect */, - 535870922669D7A800D05A09 /* CombineMoya */, - 535870942669D7A800D05A09 /* Moya */, - 535870962669D7A800D05A09 /* ReactiveMoya */, - 535870982669D7A800D05A09 /* RxMoya */, 5358709A2669D7A800D05A09 /* NukeUI */, 53A431BE266B0FFE0016769F /* JellyfinAPI */, ); @@ -440,12 +358,8 @@ ); name = JellyfinPlayer; packageProductDependencies = ( - 5338F753263B65E10014BF09 /* SwiftyRequest */, 5338F756263B7E2E0014BF09 /* KeychainSwift */, - 53892779263CBFE70035E14B /* SwiftyJSON */, 53352570265EA0A0006CCA86 /* Introspect */, - 6273DD42265F4195009C1D0B /* Moya */, - 6273DD44265F4195009C1D0B /* CombineMoya */, 621C637F26672A30004216EA /* NukeUI */, 53A431BC266B0FF20016769F /* JellyfinAPI */, ); @@ -483,11 +397,8 @@ ); mainGroup = 5377CBE8263B596A003A4E83; packageReferences = ( - 5338F752263B65E10014BF09 /* XCRemoteSwiftPackageReference "SwiftyRequest" */, 5338F755263B7E2E0014BF09 /* XCRemoteSwiftPackageReference "keychain-swift" */, - 53892778263CBFE70035E14B /* XCRemoteSwiftPackageReference "SwiftyJSON" */, 5335256F265EA0A0006CCA86 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */, - 6273DD41265F4195009C1D0B /* XCRemoteSwiftPackageReference "Moya" */, 621C637E26672A30004216EA /* XCRemoteSwiftPackageReference "NukeUI" */, 53A431BB266B0FF20016769F /* XCRemoteSwiftPackageReference "jellyfin-sdk-swift" */, ); @@ -559,31 +470,21 @@ 535BAE9F2649E569005FA86D /* ItemView.swift in Sources */, 6225FCCB2663841E00E067F6 /* ParallaxHeader.swift in Sources */, 53F8377D265FF67C00F456B3 /* VideoPlayerSettingsView.swift in Sources */, - 53987CA426572C1300E7EA70 /* SeasonItemView.swift in Sources */, 53192D5D265AA78A008A4215 /* DeviceProfileBuilder.swift in Sources */, 62133890265F83A900A81A2A /* LibraryListView.swift in Sources */, - AE8C3154265D60BF008AA076 /* SettingsModel.swift in Sources */, 53892770263C25230035E14B /* NextUpView.swift in Sources */, 535BAEA5264A151C005FA86D /* VideoPlayer.swift in Sources */, 5377CC01263B596B003A4E83 /* Model.xcdatamodeld in Sources */, 53DF641E263D9C0600A7CD1A /* LibraryView.swift in Sources */, 53A089D0264DA9DA00D57806 /* MovieItemView.swift in Sources */, 53E4E649263F725B00F67C6B /* MultiSelectorView.swift in Sources */, - 53E4E647263F6CF100F67C6B /* LibraryFilterView.swift in Sources */, - 6213388E265F777C00A81A2A /* LibraryViewModel.swift in Sources */, 621338B32660A07800A81A2A /* LazyView.swift in Sources */, - 62133895266096EF00A81A2A /* LibraryListViewModel.swift in Sources */, 535870AD2669D8DD00D05A09 /* Typings.swift in Sources */, - 6273DD48265F41B3009C1D0B /* JellyfinAPIOld.swift in Sources */, 53C4404E266C75C70049424C /* HandleAPIRequestCompletion.swift in Sources */, 5377CBF7263B596A003A4E83 /* ContentView.swift in Sources */, - 53987CA82657424A00E7EA70 /* EpisodeItemView.swift in Sources */, 5389277C263CC3DB0035E14B /* BlurHashDecode.swift in Sources */, - 53987CA626572F0700E7EA70 /* SeriesItemView.swift in Sources */, 539B2DA5263BA5B8007FF1A4 /* SettingsView.swift in Sources */, - AE8C3156265D616A008AA076 /* SettingsViewModel.swift in Sources */, 5338F74E263B61370014BF09 /* ConnectToServerView.swift in Sources */, - 6273DD4E265F47B2009C1D0B /* LibrarySearchViewModel.swift in Sources */, 5377CBF5263B596A003A4E83 /* JellyfinPlayerApp.swift in Sources */, 53EE24E6265060780068F029 /* LibrarySearchView.swift in Sources */, 53892772263C8C6F0035E14B /* LoadingView.swift in Sources */, @@ -868,14 +769,6 @@ minimumVersion = 0.1.3; }; }; - 5338F752263B65E10014BF09 /* XCRemoteSwiftPackageReference "SwiftyRequest" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/Kitura/SwiftyRequest"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 3.2.200; - }; - }; 5338F755263B7E2E0014BF09 /* XCRemoteSwiftPackageReference "keychain-swift" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/evgenyneu/keychain-swift"; @@ -884,14 +777,6 @@ minimumVersion = 19.0.0; }; }; - 53892778263CBFE70035E14B /* XCRemoteSwiftPackageReference "SwiftyJSON" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/SwiftyJSON/SwiftyJSON"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 5.0.1; - }; - }; 53A431BB266B0FF20016769F /* XCRemoteSwiftPackageReference "jellyfin-sdk-swift" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/jellyfin/jellyfin-sdk-swift"; @@ -908,14 +793,6 @@ kind = branch; }; }; - 6273DD41265F4195009C1D0B /* XCRemoteSwiftPackageReference "Moya" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/Moya/Moya"; - requirement = { - kind = exactVersion; - version = "15.0.0-alpha.1"; - }; - }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -924,66 +801,26 @@ package = 5335256F265EA0A0006CCA86 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */; productName = Introspect; }; - 5338F753263B65E10014BF09 /* SwiftyRequest */ = { - isa = XCSwiftPackageProductDependency; - package = 5338F752263B65E10014BF09 /* XCRemoteSwiftPackageReference "SwiftyRequest" */; - productName = SwiftyRequest; - }; 5338F756263B7E2E0014BF09 /* KeychainSwift */ = { isa = XCSwiftPackageProductDependency; package = 5338F755263B7E2E0014BF09 /* XCRemoteSwiftPackageReference "keychain-swift" */; productName = KeychainSwift; }; - 5358708A2669D7A800D05A09 /* SwiftyRequest */ = { - isa = XCSwiftPackageProductDependency; - package = 5338F752263B65E10014BF09 /* XCRemoteSwiftPackageReference "SwiftyRequest" */; - productName = SwiftyRequest; - }; 5358708C2669D7A800D05A09 /* KeychainSwift */ = { isa = XCSwiftPackageProductDependency; package = 5338F755263B7E2E0014BF09 /* XCRemoteSwiftPackageReference "keychain-swift" */; productName = KeychainSwift; }; - 5358708E2669D7A800D05A09 /* SwiftyJSON */ = { - isa = XCSwiftPackageProductDependency; - package = 53892778263CBFE70035E14B /* XCRemoteSwiftPackageReference "SwiftyJSON" */; - productName = SwiftyJSON; - }; 535870902669D7A800D05A09 /* Introspect */ = { isa = XCSwiftPackageProductDependency; package = 5335256F265EA0A0006CCA86 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */; productName = Introspect; }; - 535870922669D7A800D05A09 /* CombineMoya */ = { - isa = XCSwiftPackageProductDependency; - package = 6273DD41265F4195009C1D0B /* XCRemoteSwiftPackageReference "Moya" */; - productName = CombineMoya; - }; - 535870942669D7A800D05A09 /* Moya */ = { - isa = XCSwiftPackageProductDependency; - package = 6273DD41265F4195009C1D0B /* XCRemoteSwiftPackageReference "Moya" */; - productName = Moya; - }; - 535870962669D7A800D05A09 /* ReactiveMoya */ = { - isa = XCSwiftPackageProductDependency; - package = 6273DD41265F4195009C1D0B /* XCRemoteSwiftPackageReference "Moya" */; - productName = ReactiveMoya; - }; - 535870982669D7A800D05A09 /* RxMoya */ = { - isa = XCSwiftPackageProductDependency; - package = 6273DD41265F4195009C1D0B /* XCRemoteSwiftPackageReference "Moya" */; - productName = RxMoya; - }; 5358709A2669D7A800D05A09 /* NukeUI */ = { isa = XCSwiftPackageProductDependency; package = 621C637E26672A30004216EA /* XCRemoteSwiftPackageReference "NukeUI" */; productName = NukeUI; }; - 53892779263CBFE70035E14B /* SwiftyJSON */ = { - isa = XCSwiftPackageProductDependency; - package = 53892778263CBFE70035E14B /* XCRemoteSwiftPackageReference "SwiftyJSON" */; - productName = SwiftyJSON; - }; 53A431BC266B0FF20016769F /* JellyfinAPI */ = { isa = XCSwiftPackageProductDependency; package = 53A431BB266B0FF20016769F /* XCRemoteSwiftPackageReference "jellyfin-sdk-swift" */; @@ -999,16 +836,6 @@ package = 621C637E26672A30004216EA /* XCRemoteSwiftPackageReference "NukeUI" */; productName = NukeUI; }; - 6273DD42265F4195009C1D0B /* Moya */ = { - isa = XCSwiftPackageProductDependency; - package = 6273DD41265F4195009C1D0B /* XCRemoteSwiftPackageReference "Moya" */; - productName = Moya; - }; - 6273DD44265F4195009C1D0B /* CombineMoya */ = { - isa = XCSwiftPackageProductDependency; - package = 6273DD41265F4195009C1D0B /* XCRemoteSwiftPackageReference "Moya" */; - productName = CombineMoya; - }; /* End XCSwiftPackageProductDependency section */ /* Begin XCVersionGroup section */ diff --git a/JellyfinPlayer.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/JellyfinPlayer.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 729c4dbf..84fe0870 100644 --- a/JellyfinPlayer.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/JellyfinPlayer.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,15 +1,6 @@ { "object": { "pins": [ - { - "package": "Alamofire", - "repositoryURL": "https://github.com/Alamofire/Alamofire.git", - "state": { - "branch": null, - "revision": "f96b619bcb2383b43d898402283924b80e2c4bae", - "version": "5.4.3" - } - }, { "package": "AnyCodable", "repositoryURL": "https://github.com/Flight-School/AnyCodable", @@ -19,24 +10,6 @@ "version": "0.6.0" } }, - { - "package": "async-http-client", - "repositoryURL": "https://github.com/swift-server/async-http-client.git", - "state": { - "branch": null, - "revision": "037b70291941fe43de668066eb6fb802c5e181d2", - "version": "1.1.1" - } - }, - { - "package": "CircuitBreaker", - "repositoryURL": "https://github.com/Kitura/CircuitBreaker.git", - "state": { - "branch": null, - "revision": "915cd4ed17500784cf5bcbf2ef54a76830884c86", - "version": "5.0.200" - } - }, { "package": "Gifu", "repositoryURL": "https://github.com/kaishin/Gifu", @@ -64,24 +37,6 @@ "version": "19.0.0" } }, - { - "package": "LoggerAPI", - "repositoryURL": "https://github.com/Kitura/LoggerAPI.git", - "state": { - "branch": null, - "revision": "e82d34eab3f0b05391082b11ea07d3b70d2f65bb", - "version": "1.9.200" - } - }, - { - "package": "Moya", - "repositoryURL": "https://github.com/Moya/Moya", - "state": { - "branch": null, - "revision": "e5a28fb62dd5ff4e17b7025643366550044a40b0", - "version": "15.0.0-alpha.1" - } - }, { "package": "Nuke", "repositoryURL": "https://github.com/kean/Nuke.git", @@ -100,60 +55,6 @@ "version": null } }, - { - "package": "ReactiveSwift", - "repositoryURL": "https://github.com/Moya/ReactiveSwift.git", - "state": { - "branch": null, - "revision": "f195d82bb30e412e70446e2b4a77e1b514099e88", - "version": "6.1.0" - } - }, - { - "package": "RxSwift", - "repositoryURL": "https://github.com/ReactiveX/RxSwift.git", - "state": { - "branch": null, - "revision": "254617dd7fae0c45319ba5fbea435bf4d0e15b5d", - "version": "5.1.2" - } - }, - { - "package": "swift-log", - "repositoryURL": "https://github.com/apple/swift-log.git", - "state": { - "branch": null, - "revision": "5d66f7ba25daf4f94100e7022febf3c75e37a6c7", - "version": "1.4.2" - } - }, - { - "package": "swift-nio", - "repositoryURL": "https://github.com/apple/swift-nio.git", - "state": { - "branch": null, - "revision": "d161bf658780b209c185994528e7e24376cf7283", - "version": "2.29.0" - } - }, - { - "package": "swift-nio-extras", - "repositoryURL": "https://github.com/apple/swift-nio-extras.git", - "state": { - "branch": null, - "revision": "de1c80ad1fdff1ba772bcef6b392c3ef735f39a6", - "version": "1.8.0" - } - }, - { - "package": "swift-nio-ssl", - "repositoryURL": "https://github.com/apple/swift-nio-ssl.git", - "state": { - "branch": null, - "revision": "6363cdf6d2fb863e82434f3c4618f4e896e37569", - "version": "2.13.1" - } - }, { "package": "Introspect", "repositoryURL": "https://github.com/siteline/SwiftUI-Introspect", @@ -162,24 +63,6 @@ "revision": "2e09be8af614401bc9f87d40093ec19ce56ccaf2", "version": "0.1.3" } - }, - { - "package": "SwiftyJSON", - "repositoryURL": "https://github.com/SwiftyJSON/SwiftyJSON", - "state": { - "branch": null, - "revision": "b3dcd7dbd0d488e1a7077cb33b00f2083e382f07", - "version": "5.0.1" - } - }, - { - "package": "SwiftyRequest", - "repositoryURL": "https://github.com/Kitura/SwiftyRequest", - "state": { - "branch": null, - "revision": "2c543777a5088bed811503a68551a4b4eceac198", - "version": "3.2.200" - } } ] }, diff --git a/JellyfinPlayer/APIs/JellyfinAPIOld.swift b/JellyfinPlayer/APIs/JellyfinAPIOld.swift deleted file mode 100644 index 1c8fa433..00000000 --- a/JellyfinPlayer/APIs/JellyfinAPIOld.swift +++ /dev/null @@ -1,148 +0,0 @@ -/* JellyfinPlayer/Swiftfin is subject to the terms of the Mozilla Public - * License, v2.0. If a copy of the MPL was not distributed with this - * file, you can obtain one at https://mozilla.org/MPL/2.0/. - * - * Copyright 2021 Aiden Vigue & Jellyfin Contributors - */ - -import Foundation -import Moya - -enum ImageType: String { - case primary = "Primary" - case backdrop = "Backdrop" - case thumb = "Thumb" - case banner = "Banner" -} - -enum Field: String { - case primaryImageAspectRatio = "PrimaryImageAspectRatio" - case basicSyncInfo = "BasicSyncInfo" -} - -enum ItemType: String { - case movie = "Movie" - case series = "Series" -} - -enum SortType: String { - case name = "SortName" - case dateCreated = "DateCreated" - case datePlayed = "DatePlayed" - case premiereDate = "PremiereDate" - case runtime = "Runtime" -} - -enum ASC: String { - case descending = "Descending" - case ascending = "Ascending" -} - -enum FilterType: String { - case isFavorite = "IsFavorite" - case isUnplayed = "IsUnplayed" -} - -struct Filter { - var imageTypes: [ImageType] = [.primary, .backdrop, .thumb, .banner] - var fields: [Field] = [.primaryImageAspectRatio, .basicSyncInfo] - var itemTypes: [ItemType] = [.movie, .series] - var filterTypes = [FilterType]() - var sort: SortType? = .dateCreated - var asc: ASC? = .descending - var parentID: String? - var imageTypeLimit: Int? = 1 - var recursive = true - var genres = [String]() - var personIds = [String]() - var officialRatings = [String]() -} - -extension Filter { - var toParamters: [String: Any] { - var parameters = [String: Any]() - parameters["EnableImageTypes"] = imageTypes.map(\.rawValue).joined(separator: ",") - parameters["Fields"] = fields.map(\.rawValue).joined(separator: ",") - parameters["Filters"] = filterTypes.map(\.rawValue).joined(separator: ",") - parameters["ImageTypeLimit"] = imageTypeLimit - parameters["IncludeItemTypes"] = itemTypes.map(\.rawValue).joined(separator: ",") - parameters["ParentId"] = parentID - parameters["Recursive"] = recursive - parameters["SortBy"] = sort?.rawValue - parameters["SortOrder"] = asc?.rawValue - parameters["Genres"] = genres.joined(separator: ",") - parameters["PersonIds"] = personIds.joined(separator: ",") - parameters["OfficialRatings"] = officialRatings.joined(separator: ",") - return parameters - } -} - -enum JellyfinAPIOld { - case items(globalData: GlobalData, filter: Filter, page: Int) - case search(globalData: GlobalData, filter: Filter, searchQuery: String, page: Int) -} - -extension JellyfinAPIOld: TargetType { - - var baseURL: URL { - switch self { - case let .items(global, _, _), - let .search(global, _, _, _): - return URL(string: global.server.baseURI ?? "")! - } - } - - var path: String { - switch self { - case let .items(global, _, _), - let .search(global, _, _, _): - return "/Users/\(global.user.user_id ?? "")/Items" - } - } - - var method: Moya.Method { - switch self { - case .items, .search: - return .get - } - } - - var sampleData: Data { - "{".data(using: .utf8)! - } - - var task: Task { - switch self { - case let .search(_, filter, searchQuery, page): - var parameters = filter.toParamters - parameters["searchTerm"] = searchQuery - parameters["StartIndex"] = (page - 1) * 100 - parameters["Limit"] = 100 - return .requestParameters(parameters: parameters, encoding: URLEncoding.jellyfin) - case let .items(_, filter, page): - var parameters = filter.toParamters - parameters["StartIndex"] = (page - 1) * 100 - parameters["Limit"] = 100 - return .requestParameters(parameters: parameters, encoding: URLEncoding.jellyfin) - } - } - - var headers: [String: String]? { - switch self { - case let .items(global, _, _), - let .search(global, _, _, _): - var headers = [String: String]() - headers["Content-Type"] = "application/json" - headers["Accept"] = "application/json" - headers["X-Emby-Authorization"] = global.authHeader - return headers - } - } -} - -extension URLEncoding { - - static var jellyfin: URLEncoding { - URLEncoding(destination: .methodDependent, arrayEncoding: .noBrackets, boolEncoding: .literal) - } -} diff --git a/JellyfinPlayer/ConnectToServerView.swift b/JellyfinPlayer/ConnectToServerView.swift index ab7dcd5e..b6fc26d0 100644 --- a/JellyfinPlayer/ConnectToServerView.swift +++ b/JellyfinPlayer/ConnectToServerView.swift @@ -88,14 +88,18 @@ struct ConnectToServerView: View { deviceName = deviceName.removeRegexMatches(pattern: "[^\\w\\s]"); let authHeader = "MediaBrowser Client=\"SwiftFin\", Device=\"\(deviceName)\", DeviceId=\"\(serverSkipped ? reauthDeviceID : userUUID.uuidString)\", Version=\"\(appVersion ?? "0.0.1")\""; + print(authHeader) JellyfinAPI.customHeaders["X-Emby-Authorization"] = authHeader - UserAPI.authenticateUser(userId: username, pw: password) + let x: AuthenticateUserByName = AuthenticateUserByName(username: username, pw: password, password: nil) + + UserAPI.authenticateUserByName(authenticateUserByName: x) .sink(receiveCompletion: { completion in isWorking = false HandleAPIRequestCompletion(globalData: globalData, completion: completion) }, receiveValue: { response in + isWorking = true let fetchRequest: NSFetchRequest = NSFetchRequest(entityName: "Server") let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) @@ -133,6 +137,8 @@ struct ConnectToServerView: View { globalData.authHeader = authHeader _rootIsActive.wrappedValue = false jsi.did = true + print("logged in") + isWorking = false } } catch { print("Couldn't store objects to CoreData") @@ -291,6 +297,9 @@ struct ConnectToServerView: View { .frame(width: 60, height: 60) .cornerRadius(30.0) .shadow(radius: 6) + .onAppear(perform: { + print("\(uri)/Users/\(publicUser.id!)/Images/Primary?width=200&quality=80&tag=\(publicUser.primaryImageTag!)") + }) } else { Image(systemName: "person.fill") .foregroundColor(Color(red: 1, green: 1, blue: 1).opacity(0.8)) diff --git a/JellyfinPlayer/ContentView.swift b/JellyfinPlayer/ContentView.swift index 8c5258f3..7243398c 100644 --- a/JellyfinPlayer/ContentView.swift +++ b/JellyfinPlayer/ContentView.swift @@ -34,6 +34,7 @@ struct ContentView: View { @State private var libraryPrefillID: String = "" @State private var showSettingsPopover: Bool = false @State private var viewDidLoad: Bool = false + @State private var loadState: Int = 2 func startup() { if(viewDidLoad == true) { @@ -85,21 +86,31 @@ struct ContentView: View { UserAPI.getCurrentUser() .sink(receiveCompletion: { completion in HandleAPIRequestCompletion(globalData: globalData, completion: completion) + loadState = loadState - 1 }, receiveValue: { response in libraries = response.configuration?.orderedViews ?? [] librariesShowRecentlyAdded = libraries.filter { element in return !(response.configuration?.latestItemsExcludes?.contains(element))! } + + if(loadState == 0) { + isLoading = false + } }) .store(in: &globalData.pendingAPIRequests) UserViewsAPI.getUserViews(userId: globalData.user.user_id ?? "") .sink(receiveCompletion: { completion in HandleAPIRequestCompletion(globalData: globalData, completion: completion) + loadState = loadState - 1 }, receiveValue: { response in response.items?.forEach({ item in library_names[item.id ?? ""] = item.name }) + + if(loadState == 0) { + isLoading = false + } }) .store(in: &globalData.pendingAPIRequests) @@ -110,8 +121,6 @@ struct ContentView: View { if defaults.integer(forKey: "OutOfNetworkBandwidth") == 0 { defaults.setValue(40_000_000, forKey: "OutOfNetworkBandwidth") } - - isLoading = false } } @@ -132,61 +141,67 @@ struct ContentView: View { } else { if !jsi.did { LoadingView(isShowing: $isLoading) { - TabView(selection: $tabSelection) { - NavigationView { - VStack(alignment: .leading) { - ScrollView { - Spacer().frame(height: orientationInfo.orientation == .portrait ? 0 : 16) - ContinueWatchingView() - NextUpView() - ForEach(librariesShowRecentlyAdded, id: \.self) { library_id in - VStack(alignment: .leading) { - HStack { - Text("Latest \(library_names[library_id] ?? "")").font(.title2).fontWeight(.bold) - .padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 16)) - Spacer() - NavigationLink(destination: LazyView { - LibraryView(viewModel: .init(filter: Filter(parentID: library_id)), - title: library_names[library_id] ?? "") - }) { - Text("See All").font(.subheadline).fontWeight(.bold) + VStack() { + if(loadState == 0) { + TabView(selection: $tabSelection) { + NavigationView { + VStack(alignment: .leading) { + ScrollView { + Spacer().frame(height: orientationInfo.orientation == .portrait ? 0 : 16) + ContinueWatchingView() + NextUpView() + ForEach(librariesShowRecentlyAdded, id: \.self) { library_id in + VStack(alignment: .leading) { + HStack { + Text("Latest \(library_names[library_id] ?? "")").font(.title2).fontWeight(.bold) + .padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 16)) + Spacer() + NavigationLink(destination: LazyView { + LibraryView(usingParentID: library_id, + title: library_names[library_id] ?? "") + }) { + Text("See All").font(.subheadline).fontWeight(.bold) + } + }.padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16)) + LatestMediaView(usingParentID: library_id) + }.padding(EdgeInsets(top: 4, leading: 0, bottom: 0, trailing: 0)) + } + Spacer().frame(height: UIDevice.current.userInterfaceIdiom == .phone ? 20 : 30) + } + .navigationTitle("Home") + .toolbar { + ToolbarItemGroup(placement: .navigationBarTrailing) { + Button { + showSettingsPopover = true + } label: { + Image(systemName: "gear") } - }.padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16)) - LatestMediaView(usingLibraryID: library_id) - }.padding(EdgeInsets(top: 4, leading: 0, bottom: 0, trailing: 0)) - } - Spacer().frame(height: UIDevice.current.userInterfaceIdiom == .phone ? 20 : 30) - } - .navigationTitle("Home") - .toolbar { - ToolbarItemGroup(placement: .navigationBarTrailing) { - Button { - showSettingsPopover = true - } label: { - Image(systemName: "gear") + } + } + .fullScreenCover(isPresented: $showSettingsPopover) { + SettingsView(viewModel: SettingsViewModel(), close: $showSettingsPopover) } } } - .fullScreenCover(isPresented: $showSettingsPopover) { - SettingsView(viewModel: SettingsViewModel(), close: $showSettingsPopover) + .navigationViewStyle(StackNavigationViewStyle()) + .tabItem { + Text("Home") + Image(systemName: "house") } + .tag("Home") + NavigationView { + LibraryListView(libraries: library_names) + } + .navigationViewStyle(StackNavigationViewStyle()) + .tabItem { + Text("All Media") + Image(systemName: "folder") + } + .tag("All Media") } + } else { + Text("Loading...") } - .navigationViewStyle(StackNavigationViewStyle()) - .tabItem { - Text("Home") - Image(systemName: "house") - } - .tag("Home") - NavigationView { - LibraryListView(viewModel: .init(libraryNames: library_names, libraryIDs: libraries)) - } - .navigationViewStyle(StackNavigationViewStyle()) - .tabItem { - Text("All Media") - Image(systemName: "folder") - } - .tag("All Media") } } .environmentObject(globalData) diff --git a/JellyfinPlayer/ContinueWatchingView.swift b/JellyfinPlayer/ContinueWatchingView.swift index 431fde2b..faceadb2 100644 --- a/JellyfinPlayer/ContinueWatchingView.swift +++ b/JellyfinPlayer/ContinueWatchingView.swift @@ -52,7 +52,7 @@ struct ContinueWatchingView: View { LazyHStack() { Spacer().frame(width:14) ForEach(items, id: \.id) { item in - NavigationLink(destination: EmptyView()) { + NavigationLink(destination: ItemView(item: item)) { VStack(alignment: .leading) { Spacer().frame(height: 10) LazyImage(source: item.getBackdropImage(baseURL: globalData.server.baseURI ?? "", maxWidth: 320)) diff --git a/JellyfinPlayer/ItemView.swift b/JellyfinPlayer/ItemView.swift index 393c67dd..81dd16d4 100644 --- a/JellyfinPlayer/ItemView.swift +++ b/JellyfinPlayer/ItemView.swift @@ -17,53 +17,79 @@ class VideoPlayerItem: ObservableObject { } struct ItemView: View { - private var item: BaseItemDto; - @StateObject private var videoPlayerItem: VideoPlayerItem = VideoPlayerItem() + @EnvironmentObject private var globalData: GlobalData - @State private var isLoading: Bool = false; //This variable is only changed by the underlying VLC view. + @State private var fullItem: BaseItemDto = BaseItemDto(); + private var item: BaseItemDto; + + @StateObject private var videoPlayerItem: VideoPlayerItem = VideoPlayerItem() + @State private var videoIsLoading: Bool = false; //This variable is only changed by the underlying VLC view. + @State private var isLoading: Bool = false; init(item: BaseItemDto) { - self.item = item; + self.item = item + } + + func onAppear() { + isLoading = true; + UserLibraryAPI.getItem(userId: globalData.user.user_id!, itemId: item.id!) + .sink(receiveCompletion: { completion in + HandleAPIRequestCompletion(globalData: globalData, completion: completion) + }, receiveValue: { response in + isLoading = false + fullItem = response + }) + .store(in: &globalData.pendingAPIRequests) } var body: some View { - if(videoPlayerItem.shouldShowPlayer) { - LoadingViewNoBlur(isShowing: $isLoading) { - VLCPlayerWithControls(item: playback.itemToPlay, loadBinding: $isLoading, pBinding: _videoPlayerItem.projectedValue.shouldShowPlayer) - }.navigationBarHidden(true) - .navigationBarBackButtonHidden(true) - .statusBar(hidden: true) - .prefersHomeIndicatorAutoHidden(true) - .preferredColorScheme(.dark) - .edgesIgnoringSafeArea(.all) - .overrideViewPreference(.unspecified) - .supportedOrientations(.landscape) - } else { - Group { - if(item.Type == "Movie") { - MovieItemView(item: self.item) - } else if(item.Type == "Season") { - SeasonItemView(item: self.item) - } else if(item.Type == "Series") { - SeriesItemView(item: self.item) - } else if(item.Type == "Episode") { - EpisodeItemView(item: self.item) + VStack { + if(videoPlayerItem.shouldShowPlayer) { + LoadingViewNoBlur(isShowing: $videoIsLoading) { + VLCPlayerWithControls(item: videoPlayerItem.itemToPlay, loadBinding: $videoIsLoading, pBinding: _videoPlayerItem.projectedValue.shouldShowPlayer) + }.navigationBarHidden(true) + .navigationBarBackButtonHidden(true) + .statusBar(hidden: true) + .prefersHomeIndicatorAutoHidden(true) + .preferredColorScheme(.dark) + .edgesIgnoringSafeArea(.all) + .overrideViewPreference(.unspecified) + .supportedOrientations(.landscape) + } else { + if(isLoading) { + ProgressView() } else { - Text("Type: \(item.Type) not implemented yet :(") + VStack { + if(fullItem.type == "Movie") { + MovieItemView(item: fullItem) + } else if(fullItem.type == "Season") { + EmptyView() + //SeasonItemView(item: fullItem) + } else if(fullItem.type == "Series") { + EmptyView() + //SeriesItemView(item: fullItem) + } else if(fullItem.type == "Episode") { + EmptyView() + //EpisodeItemView(item: fullItem) + } else { + Text("Type: \(fullItem.type ?? "") not implemented yet :(") + } + } + .introspectTabBarController { (UITabBarController) in + UITabBarController.tabBar.isHidden = false + } + .navigationBarHidden(false) + .navigationBarBackButtonHidden(false) + .statusBar(hidden: false) + .prefersHomeIndicatorAutoHidden(false) + .preferredColorScheme(.none) + .edgesIgnoringSafeArea([]) + .overrideViewPreference(.unspecified) + .supportedOrientations(.allButUpsideDown) + .environmentObject(videoPlayerItem) } } - .introspectTabBarController { (UITabBarController) in - UITabBarController.tabBar.isHidden = false - } - .navigationBarHidden(false) - .navigationBarBackButtonHidden(false) - .statusBar(hidden: false) - .prefersHomeIndicatorAutoHidden(false) - .preferredColorScheme(.none) - .edgesIgnoringSafeArea([]) - .overrideViewPreference(.unspecified) - .supportedOrientations(.allButUpsideDown) - .environmentObject(playback) } + .onAppear(perform: onAppear) } } diff --git a/JellyfinPlayer/LatestMediaView.swift b/JellyfinPlayer/LatestMediaView.swift index cf6b7c4b..2ac3c387 100644 --- a/JellyfinPlayer/LatestMediaView.swift +++ b/JellyfinPlayer/LatestMediaView.swift @@ -16,8 +16,8 @@ struct LatestMediaView: View { private var library_id: String = ""; @State private var viewDidLoad: Bool = false; - init(usingLibraryID: String) { - library_id = usingLibraryID; + init(usingParentID: String) { + library_id = usingParentID; } func onAppear() { @@ -41,7 +41,7 @@ struct LatestMediaView: View { Spacer().frame(width:16) ForEach(items, id: \.id) { item in if(item.type == "Series" || item.type == "Movie") { - NavigationLink(destination: EmptyView()) { + NavigationLink(destination: ItemView(item: item)) { VStack(alignment: .leading) { Spacer().frame(height:10) LazyImage(source: item.getPrimaryImage(baseURL: globalData.server.baseURI!, maxWidth: 100)) diff --git a/JellyfinPlayer/LibraryFilterView.swift b/JellyfinPlayer/LibraryFilterView.swift index e98d28ee..61996d40 100644 --- a/JellyfinPlayer/LibraryFilterView.swift +++ b/JellyfinPlayer/LibraryFilterView.swift @@ -6,99 +6,15 @@ */ import SwiftUI -import SwiftyJSON -import SwiftyRequest - -struct Genre: Hashable, Identifiable { - var name: String - var id: String { name } -} +import JellyfinAPI struct LibraryFilterView: View { - @Environment(\.presentationMode) - var presentationMode - @Environment(\.managedObjectContext) - private var viewContext - @EnvironmentObject - var globalData: GlobalData - - @State - var library: String - - @Binding - var filter: Filter - @State - private var isLoading: Bool = true - @State - private var onlyUnplayed: Bool = false - @State - private var allGenres: [Genre] = [] - @State - private var selectedGenres: Set = [] - - @State - private var allRatings: [Genre] = [] - @State - private var selectedRatings: Set = [] - @State - private var sortBySelection: String = "SortName" - @State - private var sortOrder: String = "Descending" - @State - private var viewDidLoad: Bool = false - - func onAppear() { - if _viewDidLoad.wrappedValue == true { - return - } - _viewDidLoad.wrappedValue = true - if filter.filterTypes.contains(.isUnplayed) { - _onlyUnplayed.wrappedValue = true - } - if !filter.genres.isEmpty { - _selectedGenres.wrappedValue = Set(filter.genres.map { Genre(name: $0) }) - } - if !filter.officialRatings.isEmpty { - _selectedRatings.wrappedValue = Set(filter.officialRatings.map { Genre(name: $0) }) - } - _sortBySelection.wrappedValue = filter.sort?.rawValue ?? sortBySelection - _sortOrder.wrappedValue = filter.asc?.rawValue ?? sortOrder - - _allGenres.wrappedValue = [] - let url = "/Items/Filters?UserId=\(globalData.user.user_id ?? "")&ParentId=\(library)" - let request = RestRequest(method: .get, url: (globalData.server.baseURI ?? "") + url) - request.headerParameters["X-Emby-Authorization"] = globalData.authHeader - request.contentType = "application/json" - request.acceptType = "application/json" - - request.responseData { (result: Result, RestError>) in - switch result { - case let .success(response): - let body = response.body - do { - let json = try JSON(data: body) - let arr = json["Genres"].arrayObject as? [String] ?? [] - for genreName in arr { - // print(genreName) - let genre = Genre(name: genreName) - allGenres.append(genre) - } - - let arr2 = json["OfficialRatings"].arrayObject as? [String] ?? [] - for genreName in arr2 { - // print(genreName) - let genre = Genre(name: genreName) - allRatings.append(genre) - } - } catch {} - case let .failure(error): - debugPrint(error) - } - isLoading = false - } - } + @EnvironmentObject var globalData: GlobalData + @Binding var filter: Filter var body: some View { + EmptyView() + /* NavigationView { LoadingView(isShowing: $isLoading) { Form { @@ -159,5 +75,6 @@ struct LibraryFilterView: View { } } } + */ } } diff --git a/JellyfinPlayer/LibraryListView.swift b/JellyfinPlayer/LibraryListView.swift index 776eb428..355c12d6 100644 --- a/JellyfinPlayer/LibraryListView.swift +++ b/JellyfinPlayer/LibraryListView.swift @@ -9,34 +9,59 @@ import Foundation import SwiftUI struct LibraryListView: View { - @Environment(\.managedObjectContext) - private var viewContext - @EnvironmentObject - var globalData: GlobalData - @StateObject - var viewModel: LibraryListViewModel - + @EnvironmentObject var globalData: GlobalData + + @State var library_ids: [String] = ["favorites", "genres"] + @State var library_names: [String: String] = ["favorites": "Favorites", "genres": "Genres"] + var libraries: [String: String] = [:] //input libraries + + init(libraries: [String: String]) { + self.libraries = libraries + print(libraries) + } + + func onAppear() { + if(library_ids.count == 2) { + libraries.forEach() { k,v in + print("\(k): \(v)") + _library_ids.wrappedValue.append(k) + _library_names.wrappedValue[k] = v + } + print(library_ids) + print(library_names) + } + } + var body: some View { - List(viewModel.libraryIDs, id: \.self) { id in - switch id { - case "favorites": - NavigationLink(destination: LazyView { LibraryView(viewModel: .init(filter: Filter(filterTypes: [.isFavorite])), - title: viewModel.libraryNames[id] ?? "") }) { - Text(viewModel.libraryNames[id] ?? "").foregroundColor(Color.primary) - } - case "genres": - Text(viewModel.libraryNames[id] ?? "").foregroundColor(Color.primary) - default: - NavigationLink(destination: LazyView { LibraryView(viewModel: .init(filter: Filter(parentID: id)), - title: viewModel.libraryNames[id] ?? "") }) { - Text(viewModel.libraryNames[id] ?? "").foregroundColor(Color.primary) - } + List(library_ids, id: \.self) { key in + switch key { + case "favorites": + NavigationLink(destination: LazyView { + LibraryView(usingParentID: "", title: library_names[key] ?? "", filters: [.isFavorite]) + }) { + Text(library_names[key] ?? "") + } + case "genres": + NavigationLink(destination: LazyView { + EmptyView() + }) { + Text(library_names[key] ?? "") + } + default: + NavigationLink(destination: LazyView { + LibraryView(usingParentID: key, title: library_names[key] ?? "") + }) { + Text(library_names[key] ?? "") + } } } .navigationTitle("All Media") + .onAppear(perform: onAppear) .toolbar { ToolbarItemGroup(placement: .navigationBarTrailing) { - NavigationLink(destination: LazyView { LibrarySearchView(viewModel: .init(filter: .init())) }) { + NavigationLink(destination: LazyView { + LibrarySearchView(usingParentID: "") + }) { Image(systemName: "magnifyingglass") } } diff --git a/JellyfinPlayer/LibrarySearchView.swift b/JellyfinPlayer/LibrarySearchView.swift index 6f44d548..43500297 100644 --- a/JellyfinPlayer/LibrarySearchView.swift +++ b/JellyfinPlayer/LibrarySearchView.swift @@ -6,37 +6,39 @@ */ import SwiftUI -import SwiftyJSON -import SwiftyRequest import NukeUI +import JellyfinAPI struct LibrarySearchView: View { - @Environment(\.managedObjectContext) - private var viewContext - @EnvironmentObject - var globalData: GlobalData - @StateObject - var viewModel: LibrarySearchViewModel + @EnvironmentObject var globalData: GlobalData + @EnvironmentObject var orientationInfo: OrientationInfo - @State - private var tracks: [GridItem] = [] - - @Environment(\.verticalSizeClass) - var verticalSizeClass: UserInterfaceSizeClass? - @Environment(\.horizontalSizeClass) - var horizontalSizeClass: UserInterfaceSizeClass? + @State private var items: [BaseItemDto] = [] + @State private var searchQuery: String = "" + @State private var isLoading: Bool = false + + var usingParentID: String func onAppear() { - guard viewModel.globalData != globalData else { return } recalcTracks() - viewModel.globalData = globalData + requestSearch(query: "") } - - var isPortrait: Bool { - let result = verticalSizeClass == .regular && horizontalSizeClass == .compact - return result + + func requestSearch(query: String) { + isLoading = true + ItemsAPI.getItems(userId: globalData.user.user_id!, searchTerm: query, parentId: usingParentID) + .sink(receiveCompletion: { completion in + HandleAPIRequestCompletion(globalData: globalData, completion: completion) + }, receiveValue: { response in + items = response.items! + }) + .store(in: &globalData.pendingAPIRequests) + + isLoading = false } - + + //MARK: tracks for grid + @State private var tracks: [GridItem] = [] func recalcTracks() { let trkCnt = Int(floor(UIScreen.main.bounds.size.width / 125)) _tracks.wrappedValue = [] @@ -47,96 +49,56 @@ struct LibrarySearchView: View { var body: some View { ZStack { - VStack { - Spacer().frame(height: 6) - TextField("Search", text: $viewModel.searchQuery, onEditingChanged: { _ in - print("changed") - }) - .padding(.horizontal, 10) - .foregroundColor(Color.secondary) - .textFieldStyle(RoundedBorderTextFieldStyle()) - ScrollView(.vertical) { - LazyVGrid(columns: tracks) { - ForEach(viewModel.items, id: \.Id) { item in - NavigationLink(destination: ItemView(item: item)) { - ResumeItemGridCell(item: item) + if(isLoading == true) { + ProgressView() + } + if(!items.isEmpty) { + VStack { + Spacer().frame(height: 6) + TextField("Search", text: $searchQuery) + .padding(.horizontal, 10) + .foregroundColor(Color.secondary) + .textFieldStyle(RoundedBorderTextFieldStyle()) + ScrollView(.vertical) { + LazyVGrid(columns: tracks) { + ForEach(items, id: \.id) { item in + NavigationLink(destination: ItemView(item: item)) { + VStack(alignment: .leading) { + LazyImage(source: item.getPrimaryImage(baseURL: globalData.server.baseURI!, maxWidth: 100)) + .placeholderAndFailure { + Image(uiImage: UIImage(blurHash: item.getPrimaryImageBlurHash(), + size: CGSize(width: 32, height: 32))!) + .resizable() + .frame(width: 100, height: 150) + .cornerRadius(10) + } + .frame(width: 100, height: 150) + .cornerRadius(10) + Text(item.name!) + .font(.caption) + .fontWeight(.semibold) + .foregroundColor(.primary) + .lineLimit(1) + Text(String(item.productionYear!)) + .foregroundColor(.secondary) + .font(.caption) + .fontWeight(.medium) + }.frame(width: 100) + } } + }.onChange(of: orientationInfo.orientation) { _ in + recalcTracks() } - }.onChange(of: isPortrait) { _ in - recalcTracks() } } - } - if viewModel.isLoading { - ProgressView() - } else if viewModel.items.isEmpty { - Text("Empty Response") + } else { + Text("No results found :(") } } .onAppear(perform: onAppear) .navigationBarTitle("Search", displayMode: .inline) - } -} - -struct ResumeItemGridCell: View { - @EnvironmentObject - var globalData: GlobalData - - var item: ResumeItem - - var body: some View { - VStack(alignment: .leading) { - if item.Type == "Movie" { - LazyImage(source: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?fillWidth=300&fillHeight=450&quality=90&tag=\(item.Image)")) - .placeholderAndFailure { - Image(uiImage: UIImage(blurHash: item - .BlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item - .BlurHash, - size: CGSize(width: 32, height: 32))!) - .resizable() - .frame(width: 100, height: 150) - .cornerRadius(10) - } - .frame(width: 100, height: 150) - .cornerRadius(10) - } else { - LazyImage(source: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?fillWidth=300&fillHeight=450&quality=90&tag=\(item.Image)")) - .placeholderAndFailure { - Image(uiImage: UIImage(blurHash: item - .BlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item - .BlurHash, - size: CGSize(width: 32, height: 32))!) - .resizable() - .frame(width: 100, height: 150) - .cornerRadius(10) - } - .frame(width: 100, height: 150) - .cornerRadius(10).overlay(ZStack { - if item.ItemBadge == 0 { - Image(systemName: "checkmark") - .font(.caption) - .padding(3) - .foregroundColor(.white) - } else { - Text("\(String(item.ItemBadge ?? 0))") - .font(.caption) - .padding(3) - .foregroundColor(.white) - } - }.background(Color.black) - .opacity(0.8) - .cornerRadius(10.0) - .padding(3), alignment: .topTrailing) - } - Text(item.Name) - .font(.caption) - .fontWeight(.semibold) - .foregroundColor(.primary) - .lineLimit(1) - Text(String(item.ProductionYear)) - .foregroundColor(.secondary) - .font(.caption) - .fontWeight(.medium) - }.frame(width: 100) + .onChange(of: searchQuery) { query in + requestSearch(query: query) + } } } diff --git a/JellyfinPlayer/LibraryView.swift b/JellyfinPlayer/LibraryView.swift index 1d72c971..ad751a40 100644 --- a/JellyfinPlayer/LibraryView.swift +++ b/JellyfinPlayer/LibraryView.swift @@ -7,46 +7,69 @@ import SwiftUI import NukeUI +import JellyfinAPI struct LibraryView: View { - @Environment(\.managedObjectContext) - private var viewContext - @EnvironmentObject - var globalData: GlobalData - @StateObject - var viewModel: LibraryViewModel - - @State - private var showFiltersPopover: Bool = false - @State - private var showingSearchView: Bool = false - - private var title: String - - @State - private var tracks: [GridItem] = [] - - init(viewModel: LibraryViewModel, title: String) { - _viewModel = StateObject(wrappedValue: viewModel) + @EnvironmentObject var globalData: GlobalData + @EnvironmentObject var orientationInfo: OrientationInfo + + @State private var items: [BaseItemDto] = [] + @State private var isLoading: Bool = false + + var usingParentID: String = "" + var title: String = "" + var filters: [ItemFilter] = [] + var personId: String = "" + var genre: String = "" + var studio: String = "" + + init(usingParentID: String, title: String) { + self.usingParentID = usingParentID self.title = title } - + + init(usingParentID: String, title: String, filters: [ItemFilter]) { + self.usingParentID = usingParentID + self.title = title + self.filters = filters + } + + init(withPerson: BaseItemPerson) { + self.usingParentID = "" + self.title = withPerson.name ?? "" + self.personId = withPerson.id! + } + + init(withGenre: NameGuidPair) { + self.usingParentID = "" + self.title = withGenre.name ?? "" + self.genre = withGenre.id ?? "" + } + + init(withStudio: NameGuidPair) { + self.usingParentID = "" + self.title = withStudio.name ?? "" + self.studio = withStudio.id ?? "" + } + func onAppear() { - guard viewModel.globalData != globalData else { return } recalcTracks() - viewModel.globalData = globalData + isLoading = true + items = [] + + ItemsAPI.getItemsByUserId(userId: globalData.user.user_id!, limit: 100, recursive: true, searchTerm: nil, sortOrder: [.ascending], fields: [.parentId,.primaryImageAspectRatio,.basicSyncInfo], includeItemTypes: ["Movie","Series"], filters: filters, enableUserData: true, personIds: (personId == "" ? nil : [personId]), studioIds: (studio == "" ? nil : [studio]), genreIds: (genre == "" ? nil : [genre]), enableImages: true) + .sink(receiveCompletion: { completion in + HandleAPIRequestCompletion(globalData: globalData, completion: completion) + isLoading = false + }, receiveValue: { response in + items = response.items ?? [] + isLoading = false + }) + .store(in: &globalData.pendingAPIRequests) } - - @Environment(\.verticalSizeClass) - var verticalSizeClass: UserInterfaceSizeClass? - @Environment(\.horizontalSizeClass) - var horizontalSizeClass: UserInterfaceSizeClass? - - var isPortrait: Bool { - let result = verticalSizeClass == .regular && horizontalSizeClass == .compact - return result - } - + + //MARK: tracks for grid + @State private var tracks: [GridItem] = [] func recalcTracks() { let trkCnt = Int(floor(UIScreen.main.bounds.size.width / 125)) _tracks.wrappedValue = [] @@ -57,136 +80,50 @@ struct LibraryView: View { var body: some View { ZStack { - ScrollView(.vertical) { - Spacer().frame(height: 16) - LazyVGrid(columns: tracks) { - ForEach(viewModel.items, id: \.Id) { item in - NavigationLink(destination: ItemView(item: item)) { - ItemGridView(item: item) + if(isLoading == true) { + ProgressView() + } else { + if(!items.isEmpty) { + VStack { + ScrollView(.vertical) { + Spacer().frame(height: 16) + LazyVGrid(columns: tracks) { + ForEach(items, id: \.id) { item in + NavigationLink(destination: ItemView(item: item)) { + VStack(alignment: .leading) { + LazyImage(source: item.getPrimaryImage(baseURL: globalData.server.baseURI!, maxWidth: 100)) + .placeholderAndFailure { + Image(uiImage: UIImage(blurHash: item.getPrimaryImageBlurHash(), + size: CGSize(width: 32, height: 32))!) + .resizable() + .frame(width: 100, height: 150) + .cornerRadius(10) + } + .frame(width: 100, height: 150) + .cornerRadius(10) + Text(item.name ?? "") + .font(.caption) + .fontWeight(.semibold) + .foregroundColor(.primary) + .lineLimit(1) + Text(String(item.productionYear ?? 0)) + .foregroundColor(.secondary) + .font(.caption) + .fontWeight(.medium) + }.frame(width: 100) + } + } + }.onChange(of: orientationInfo.orientation) { _ in + recalcTracks() + } } } + } else { + Text("No results found :(") } - HStack() { - Spacer() - Button { - viewModel.requestPreviousPage() - } label: { - Image(systemName: "chevron.left").font(.system(size: 25)) - }.disabled(viewModel.isHiddenPreviousButton) - Text("\(viewModel.page) of \(viewModel.totalPages)") - Button { - viewModel.requestNextPage() - } label: { - Image(systemName: "chevron.right").font(.system(size: 25)) - }.disabled(viewModel.isHiddenNextButton) - Spacer() - } - Spacer().frame(height: 16) - } - .onChange(of: isPortrait) { _ in - recalcTracks() - } - if viewModel.isLoading { - ProgressView() - } else if viewModel.items.isEmpty { - Text("Empty Response") } } .onAppear(perform: onAppear) - .navigationTitle(title) - .toolbar { - ToolbarItemGroup(placement: .navigationBarTrailing) { - if !viewModel.isHiddenPreviousButton { - Button { - viewModel.requestPreviousPage() - } label: { - Image(systemName: "chevron.left") - } - } - if !viewModel.isHiddenNextButton { - Button { - viewModel.requestNextPage() - } label: { - Image(systemName: "chevron.right") - } - } - NavigationLink(destination: LazyView { LibrarySearchView(viewModel: .init(filter: viewModel.filter)) }) { - Image(systemName: "magnifyingglass") - } - Button { - showFiltersPopover = true - } label: { - Image(systemName: "line.horizontal.3.decrease") - } - } - } - .sheet(isPresented: self.$showFiltersPopover) { - LibraryFilterView(library: viewModel.filter.parentID ?? "", filter: $viewModel.filter) - .environmentObject(self.globalData) - } - } -} - -extension LibraryView { - struct ItemGridView: View { - @EnvironmentObject - var globalData: GlobalData - var item: ResumeItem - - var body: some View { - VStack(alignment: .leading) { - if item.Type == "Movie" { - LazyImage(source: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?maxWidth=250&quality=80&tag=\(item.Image)")) - .placeholderAndFailure { - Image(uiImage: UIImage(blurHash: item - .BlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item - .BlurHash, - size: CGSize(width: 16, height: 16))!) - .resizable() - .frame(width: 100, height: 150) - .cornerRadius(10) - } - .frame(width: 100, height: 150) - .cornerRadius(10) - } else { - LazyImage(source: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?maxWidth=250&quality=80&tag=\(item.Image)")) - .placeholderAndFailure { - Image(uiImage: UIImage(blurHash: item - .BlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item - .BlurHash, - size: CGSize(width: 16, height: 16))!) - .resizable() - .frame(width: 100, height: 150) - .cornerRadius(10) - } - .frame(width: 100, height: 150) - .cornerRadius(10).overlay(ZStack { - if item.ItemBadge == 0 { - Image(systemName: "checkmark") - .font(.caption) - .padding(3) - .foregroundColor(.white) - } else { - Text("\(String(item.ItemBadge ?? 0))") - .font(.caption) - .padding(3) - .foregroundColor(.white) - } - }.background(Color.black) - .opacity(0.8) - .cornerRadius(10.0) - .padding(3), alignment: .topTrailing) - } - Text(item.Name) - .font(.caption) - .fontWeight(.semibold) - .foregroundColor(.primary) - .lineLimit(1) - Text(String(item.ProductionYear)) - .foregroundColor(.secondary) - .font(.caption) - .fontWeight(.medium) - }.frame(width: 100) - } + .navigationBarTitle(title, displayMode: .inline) } } diff --git a/JellyfinPlayer/Views/LoadingView.swift b/JellyfinPlayer/LoadingView.swift similarity index 100% rename from JellyfinPlayer/Views/LoadingView.swift rename to JellyfinPlayer/LoadingView.swift diff --git a/JellyfinPlayer/Models/SettingsModel.swift b/JellyfinPlayer/Models/SettingsModel.swift deleted file mode 100644 index a0622bf9..00000000 --- a/JellyfinPlayer/Models/SettingsModel.swift +++ /dev/null @@ -1,22 +0,0 @@ -/* JellyfinPlayer/Swiftfin is subject to the terms of the Mozilla Public - * License, v2.0. If a copy of the MPL was not distributed with this - * file, you can obtain one at https://mozilla.org/MPL/2.0/. - * - * Copyright 2021 Aiden Vigue & Jellyfin Contributors - */ - -import Foundation - -struct UserSettings: Decodable { - var LocalMaxBitrate: Int; - var RemoteMaxBitrate: Int; - var AutoSelectSubtitles: Bool; - var AutoSelectSubtitlesLangcode: String; - var SubtitlePositionOffset: Int; - var SubtitleFontName: String; -} - -struct Bitrates: Codable, Hashable { - public var name: String - public var value: Int -} diff --git a/JellyfinPlayer/MovieItemView.swift b/JellyfinPlayer/MovieItemView.swift index 3666d322..d42eb9b0 100644 --- a/JellyfinPlayer/MovieItemView.swift +++ b/JellyfinPlayer/MovieItemView.swift @@ -6,146 +6,32 @@ */ import SwiftUI -import SwiftyJSON -import SwiftyRequest import NukeUI - -class DetailItem: ObservableObject { - @Published - var Name: String = "" - @Published - var Id: String = "" - @Published - var IndexNumber: Int? = nil - @Published - var ParentIndexNumber: Int? = nil - @Published - var Poster: String = "" - @Published - var Backdrop: String = "" - @Published - var PosterBlurHash: String = "" - @Published - var BackdropBlurHash: String = "" - @Published - var `Type`: String = "" - @Published - var SeasonId: String? = nil - @Published - var SeriesId: String? = nil - @Published - var SeriesName: String? = nil - @Published - var ItemProgress: Double = 0 - @Published - var ItemBadge: Int? = 0 - @Published - var ProductionYear: Int = 1999 - @Published - var Runtime: String = "" - @Published - var RuntimeTicks: Int = 0 - @Published - var Cast: [CastMember] = [] - @Published - var OfficialRating: String = "" - @Published - var Progress: Double = 0 - @Published - var Watched: Bool = false - @Published - var Overview: String = "" - @Published - var Tagline: String = "" - @Published - var Directors: [String] = [] - @Published - var Writers: [String] = [] - @Published - var CriticRating: String = "" - @Published - var CommunityRating: String = "" - @Published - var Studios: [String] = [] - @Published - var ParentId: String = "" - @Published - var Genres: [IVGenre] = [] - @Published - var ProgressStr: String = "" - @Published - var ResumeItem: ResumeItem? = nil - @Published - var ParentBackdropItemId: String = "" -} - -class IVGenre: ObservableObject { - @Published - var Id: String = "" - @Published - var Name: String = "" -} - -class CastMember: ObservableObject { - @Published - var Name: String = "" - @Published - var Role: String = "" - @Published - var ImageBlurHash: String = "" - @Published - var Id: String = "" - @Published - var Image = URL(string: "https://example.com")! -} +import JellyfinAPI struct MovieItemView: View { - @EnvironmentObject - private var globalData: GlobalData - @EnvironmentObject - private var orientationInfo: OrientationInfo - @EnvironmentObject - private var playbackInfo: ItemPlayback + @EnvironmentObject private var globalData: GlobalData + @EnvironmentObject private var orientationInfo: OrientationInfo + @EnvironmentObject private var playbackInfo: VideoPlayerItem - @State - private var isLoading: Bool = true - - var item: ResumeItem - var fullItem: DetailItem - - @State - private var progressString: String = "" - @State - private var viewDidLoad: Bool = false - @State - private var watched: Bool = false { + var item: BaseItemDto + + @State private var watched: Bool = false { didSet { if watched == true { - let date = Date() - let formatter = DateFormatter() - formatter.locale = Locale(identifier: "en_US_POSIX") - formatter.timeZone = TimeZone(secondsFromGMT: 0) - print((globalData.server.baseURI ?? "") + - "/Users/\(globalData.user.user_id ?? "")/PlayedItems/\(fullItem.Id)?DatePlayed=\(formatter.string(from: date).replacingOccurrences(of: ":", with: "%3A"))") - let request = RestRequest(method: .post, - url: (globalData.server.baseURI ?? "") + - "/Users/\(globalData.user.user_id ?? "")/PlayedItems/\(fullItem.Id)?DatePlayed=\(formatter.string(from: date).replacingOccurrences(of: ":", with: "%3A"))") - request.headerParameters["X-Emby-Authorization"] = globalData.authHeader - request.contentType = "application/json" - request.acceptType = "application/json" - - request.responseData { (_: Result, RestError>) in - } + PlaystateAPI.markPlayedItem(userId: globalData.user.user_id!, itemId: item.id!) + .sink(receiveCompletion: { completion in + HandleAPIRequestCompletion(globalData: globalData, completion: completion) + }, receiveValue: { _ in + }) + .store(in: &globalData.pendingAPIRequests) } else { - let request = RestRequest(method: .delete, - url: (globalData.server.baseURI ?? "") + - "/Users/\(globalData.user.user_id ?? "")/PlayedItems/\(fullItem.Id)") - request.headerParameters["X-Emby-Authorization"] = globalData.authHeader - request.contentType = "application/json" - request.acceptType = "application/json" - - request.responseData { (_: Result, RestError>) in - } + PlaystateAPI.markUnplayedItem(userId: globalData.user.user_id!, itemId: item.id!) + .sink(receiveCompletion: { completion in + HandleAPIRequestCompletion(globalData: globalData, completion: completion) + }, receiveValue: { _ in + }) + .store(in: &globalData.pendingAPIRequests) } } } @@ -154,150 +40,27 @@ struct MovieItemView: View { private var favorite: Bool = false { didSet { if favorite == true { - let request = RestRequest(method: .post, - url: (globalData.server.baseURI ?? "") + - "/Users/\(globalData.user.user_id ?? "")/FavoriteItems/\(fullItem.Id)") - request.headerParameters["X-Emby-Authorization"] = globalData.authHeader - request.contentType = "application/json" - request.acceptType = "application/json" - - request.responseData { (_: Result, RestError>) in - } + UserLibraryAPI.markFavoriteItem(userId: globalData.user.user_id!, itemId: item.id!) + .sink(receiveCompletion: { completion in + HandleAPIRequestCompletion(globalData: globalData, completion: completion) + }, receiveValue: { _ in + }) + .store(in: &globalData.pendingAPIRequests) } else { - let request = RestRequest(method: .delete, - url: (globalData.server.baseURI ?? "") + - "/Users/\(globalData.user.user_id ?? "")/FavoriteItems/\(fullItem.Id)") - request.headerParameters["X-Emby-Authorization"] = globalData.authHeader - request.contentType = "application/json" - request.acceptType = "application/json" - - request.responseData { (_: Result, RestError>) in - } - } - } - } - - init(item: ResumeItem) { - self.item = item - self.fullItem = DetailItem() - } - - func loadData() { - if _viewDidLoad.wrappedValue == true { - return - } - _viewDidLoad.wrappedValue = true - let url = "/Users/\(globalData.user.user_id ?? "")/Items/\(item.Id)" - - let request = RestRequest(method: .get, url: (globalData.server.baseURI ?? "") + url) - request.headerParameters["X-Emby-Authorization"] = globalData.authHeader - request.contentType = "application/json" - request.acceptType = "application/json" - - request.responseData { (result: Result, RestError>) in - switch result { - case let .success(response): - let body = response.body - do { - let json = try JSON(data: body) - fullItem.ProductionYear = json["ProductionYear"].int ?? 0 - fullItem.Poster = json["ImageTags"]["Primary"].string ?? "" - fullItem.PosterBlurHash = json["ImageBlurHashes"]["Primary"][fullItem.Poster].string ?? "" - fullItem.Backdrop = json["BackdropImageTags"][0].string ?? "" - fullItem.BackdropBlurHash = json["ImageBlurHashes"]["Backdrop"][fullItem.Backdrop].string ?? "" - fullItem.Name = json["Name"].string ?? "" - fullItem.Type = json["Type"].string ?? "" - fullItem.IndexNumber = json["IndexNumber"].int ?? nil - fullItem.Id = json["Id"].string ?? "" - fullItem.ParentIndexNumber = json["ParentIndexNumber"].int ?? nil - fullItem.SeasonId = json["SeasonId"].string ?? nil - fullItem.SeriesId = json["SeriesId"].string ?? nil - fullItem.Overview = json["Overview"].string ?? "" - fullItem.Tagline = json["Taglines"][0].string ?? "" - fullItem.SeriesName = json["SeriesName"].string ?? nil - fullItem.Progress = Double(json["UserData"]["PlaybackPositionTicks"].int ?? 0) - fullItem.OfficialRating = json["OfficialRating"].string ?? "PG-13" - fullItem.Watched = json["UserData"]["Played"].bool ?? false - fullItem.CommunityRating = String(json["CommunityRating"].float ?? 0.0) - fullItem.CriticRating = String(json["CriticRating"].int ?? 0) - fullItem.ParentId = json["ParentId"].string ?? "" - // People - fullItem.Directors = [] - fullItem.Studios = [] - fullItem.Writers = [] - fullItem.Cast = [] - fullItem.Genres = [] - - for (_, person): (String, JSON) in json["People"] { - if person["Type"].stringValue == "Director" { - fullItem.Directors.append(person["Name"].string ?? "") - } else if person["Type"].stringValue == "Writer" { - fullItem.Writers.append(person["Name"].string ?? "") - } else if person["Type"].stringValue == "Actor" { - let cast = CastMember() - cast.Name = person["Name"].string ?? "" - cast.Id = person["Id"].string ?? "" - let imageTag = person["PrimaryImageTag"].string ?? "" - cast.ImageBlurHash = person["ImageBlurHashes"]["Primary"][imageTag].string ?? "" - cast.Role = person["Role"].string ?? "" - cast - .Image = - URL(string: "\(globalData.server.baseURI ?? "")/Items/\(cast.Id)/Images/Primary?maxWidth=250&quality=85&tag=\(imageTag)")! - fullItem.Cast.append(cast) - } - } - - // Studios - for (_, studio): (String, JSON) in json["Studios"] { - fullItem.Studios.append(studio["Name"].string ?? "") - } - - // Genres - for (_, genre): (String, JSON) in json["GenreItems"] { - let tmpGenre = IVGenre() - tmpGenre.Id = genre["Id"].string ?? "" - tmpGenre.Name = genre["Name"].string ?? "" - fullItem.Genres.append(tmpGenre) - } - - _watched.wrappedValue = fullItem.Watched - _favorite.wrappedValue = json["UserData"]["IsFavorite"].bool ?? false - - // Process runtime - let seconds: Int = ((json["RunTimeTicks"].int ?? 0) / 10_000_000) - fullItem.RuntimeTicks = json["RunTimeTicks"].int ?? 0 - let hours = (seconds / 3600) - let minutes = ((seconds - (hours * 3600)) / 60) - if hours != 0 { - fullItem.Runtime = "\(hours):\(String(minutes).leftPad(toWidth: 2, withString: "0"))" - } else { - fullItem.Runtime = "\(String(minutes).leftPad(toWidth: 2, withString: "0"))m" - } - - if fullItem.Progress != 0 { - let remainingSecs = (Double(json["RunTimeTicks"].int ?? 0) - fullItem.Progress) / 10_000_000 - let proghours = Int(remainingSecs / 3600) - let progminutes = Int((Int(remainingSecs) - (proghours * 3600)) / 60) - if proghours != 0 { - _progressString.wrappedValue = "\(proghours):\(String(progminutes).leftPad(toWidth: 2, withString: "0"))" - } else { - _progressString.wrappedValue = "\(String(progminutes).leftPad(toWidth: 2, withString: "0"))m" - } - } - _isLoading.wrappedValue = false - } catch {} - case let .failure(error): - debugPrint(error) + UserLibraryAPI.unmarkFavoriteItem(userId: globalData.user.user_id!, itemId: item.id!) + .sink(receiveCompletion: { completion in + HandleAPIRequestCompletion(globalData: globalData, completion: completion) + }, receiveValue: { _ in + }) + .store(in: &globalData.pendingAPIRequests) } } } var portraitHeaderView: some View { - LazyImage(source: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(fullItem.Id)/Images/Backdrop?maxWidth=550&quality=90&tag=\(fullItem.Backdrop)")) + LazyImage(source: item.getBackdropImage(baseURL: globalData.server.baseURI!, maxWidth: 1200)) .placeholderAndFailure { - Image(uiImage: UIImage(blurHash: fullItem - .BackdropBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : fullItem - .BackdropBlurHash, + Image(uiImage: UIImage(blurHash: item.getBackdropImageBlurHash(), size: CGSize(width: 32, height: 32))!) .resizable() } @@ -309,11 +72,9 @@ struct MovieItemView: View { var portraitHeaderOverlayView: some View { VStack(alignment: .leading) { HStack(alignment: .bottom, spacing: 12) { - LazyImage(source: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(fullItem.Id)/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)")) + LazyImage(source: item.getPrimaryImage(baseURL: globalData.server.baseURI!, maxWidth: 120)) .placeholderAndFailure { - Image(uiImage: UIImage(blurHash: fullItem - .PosterBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : - fullItem.PosterBlurHash, + Image(uiImage: UIImage(blurHash: item.getPrimaryImageBlurHash(), size: CGSize(width: 32, height: 32))!) .resizable() .frame(width: 120, height: 180) @@ -324,22 +85,22 @@ struct MovieItemView: View { .cornerRadius(10) VStack(alignment: .leading) { Spacer() - Text(fullItem.Name).font(.headline) + Text(item.name!).font(.headline) .fontWeight(.semibold) .foregroundColor(.primary) .fixedSize(horizontal: false, vertical: true) .offset(y: -4) HStack { - Text(String(fullItem.ProductionYear)).font(.subheadline) + Text(String(item.productionYear ?? 0)).font(.subheadline) .fontWeight(.medium) .foregroundColor(.secondary) .lineLimit(1) - Text(fullItem.Runtime).font(.subheadline) + Text(item.getItemRuntime()).font(.subheadline) .fontWeight(.medium) .foregroundColor(.secondary) .lineLimit(1) - if fullItem.OfficialRating != "" { - Text(fullItem.OfficialRating).font(.subheadline) + if item.officialRating != "" { + Text(item.officialRating!).font(.subheadline) .fontWeight(.semibold) .foregroundColor(.secondary) .lineLimit(1) @@ -354,11 +115,11 @@ struct MovieItemView: View { HStack { // Play button Button { - self.playbackInfo.itemToPlay = fullItem - self.playbackInfo.shouldPlay = true + self.playbackInfo.itemToPlay = item + self.playbackInfo.shouldShowPlayer = true } label: { HStack { - Text(fullItem.Progress == 0 ? "Play" : "\(progressString) left") + Text(item.getItemProgressString() == "" ? "Play" : "\(item.getItemProgressString()) left") .foregroundColor(Color.white).font(.callout).fontWeight(.semibold) Image(systemName: "play.fill").foregroundColor(Color.white).font(.system(size: 20)) } @@ -398,349 +159,310 @@ struct MovieItemView: View { } var body: some View { - LoadingView(isShowing: $isLoading) { - VStack(alignment: .leading) { - if !isLoading { - if orientationInfo.orientation == .portrait { - ParallaxHeaderScrollView(header: portraitHeaderView, - staticOverlayView: portraitHeaderOverlayView, - overlayAlignment: .bottomLeading, - headerHeight: UIDevice.current - .userInterfaceIdiom == .pad ? 350 : - UIScreen.main.bounds.width * 0.5625) { - VStack(alignment: .leading) { - Spacer() - .frame(height: UIDevice.current.userInterfaceIdiom == .pad ? 135 : 40) - .padding(.bottom, UIDevice.current.userInterfaceIdiom == .pad ? 54 : 24) - if fullItem.Tagline != "" { - Text(fullItem.Tagline).font(.body).italic().padding(.top, 7) - .fixedSize(horizontal: false, vertical: true).padding(.leading, 16) - .padding(.trailing, 16) - } - Text(fullItem.Overview).font(.footnote).padding(.top, 3) - .fixedSize(horizontal: false, vertical: true).padding(.bottom, 3).padding(.leading, 16) - .padding(.trailing, 16) - if !fullItem.Genres.isEmpty { - ScrollView(.horizontal, showsIndicators: false) { - HStack { - Text("Genres:").font(.callout).fontWeight(.semibold) - ForEach(fullItem.Genres, id: \.Id) { genre in - NavigationLink(destination: LazyView { - LibraryView(viewModel: .init(filter: Filter(genres: [ - genre - .Name, - ])), - title: genre.Name) - }) { - Text(genre.Name).font(.footnote) - } - } - }.padding(.leading, 16).padding(.trailing, 16) + VStack(alignment: .leading) { + if orientationInfo.orientation == .portrait { + ParallaxHeaderScrollView(header: portraitHeaderView, staticOverlayView: portraitHeaderOverlayView, overlayAlignment: .bottomLeading, headerHeight: UIDevice.current.userInterfaceIdiom == .pad ? 350 : UIScreen.main.bounds.width * 0.5625) { + VStack(alignment: .leading) { + Spacer() + .frame(height: UIDevice.current.userInterfaceIdiom == .pad ? 135 : 40) + .padding(.bottom, UIDevice.current.userInterfaceIdiom == .pad ? 54 : 24) + if !(item.taglines ?? []).isEmpty { + Text(item.taglines!.first!).font(.body).italic().padding(.top, 7) + .fixedSize(horizontal: false, vertical: true).padding(.leading, 16) + .padding(.trailing, 16) + } + Text(item.overview ?? "").font(.footnote).padding(.top, 3) + .fixedSize(horizontal: false, vertical: true).padding(.bottom, 3).padding(.leading, 16) + .padding(.trailing, 16) + if !(item.genreItems ?? []).isEmpty { + ScrollView(.horizontal, showsIndicators: false) { + HStack { + Text("Genres:").font(.callout).fontWeight(.semibold) + ForEach(item.genreItems!, id: \.id) { genre in + NavigationLink(destination: LazyView { + LibraryView(withGenre: genre) + }) { + Text(genre.name ?? "").font(.footnote) + } } - } - if !fullItem.Cast.isEmpty { - ScrollView(.horizontal, showsIndicators: false) { - VStack { - Spacer().frame(height: 8) - HStack { - Spacer().frame(width: 16) - ForEach(fullItem.Cast, id: \.Id) { cast in - NavigationLink(destination: LazyView { - LibraryView(viewModel: .init(filter: Filter(personIds: [ - cast - .Id, - ])), title: cast.Name) - }) { - VStack { - LazyImage(source: cast.Image) - .placeholderAndFailure { - Image(uiImage: UIImage(blurHash: cast - .ImageBlurHash == "" ? - "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : - cast.ImageBlurHash, - size: CGSize(width: 16, - height: 16))!) - .resizable() - .aspectRatio(contentMode: .fill) - .frame(width: 100, height: 100) - .cornerRadius(10) - } - .contentMode(.aspectFill) - .frame(width: 100, height: 100) - .cornerRadius(10) - Text(cast.Name).font(.footnote).fontWeight(.regular).lineLimit(1) - .frame(width: 100).foregroundColor(Color.primary) - if cast.Role != "" { - Text(cast.Role).font(.caption).fontWeight(.medium).lineLimit(1) - .foregroundColor(Color.secondary).frame(width: 100) + }.padding(.leading, 16).padding(.trailing, 16) + } + } + if !(item.people ?? []).isEmpty { + ScrollView(.horizontal, showsIndicators: false) { + VStack { + Spacer().frame(height: 8) + HStack { + Spacer().frame(width: 16) + ForEach(item.people!, id: \.self) { person in + if(person.type! == "Actor") { + NavigationLink(destination: LazyView { + LibraryView(withPerson: person) + }) { + VStack { + LazyImage(source: person.getImage(baseURL: globalData.server.baseURI!, maxWidth: 100)) + .placeholderAndFailure { + Image(uiImage: UIImage(blurHash: person.getBlurHash(), + size: CGSize(width: 16, + height: 16))!) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: 100, height: 100) + .cornerRadius(10) } + .contentMode(.aspectFill) + .frame(width: 100, height: 100) + .cornerRadius(10) + Text(person.name ?? "").font(.footnote).fontWeight(.regular).lineLimit(1) + .frame(width: 100).foregroundColor(Color.primary) + if person.role != "" { + Text(person.role!).font(.caption).fontWeight(.medium).lineLimit(1) + .foregroundColor(Color.secondary).frame(width: 100) } } - Spacer().frame(width: 10) } - Spacer().frame(width: 16) + Spacer().frame(width: 10) } } - }.padding(.top, -3) + Spacer().frame(width: 16) + } } - if !fullItem.Directors.isEmpty { - HStack { - Text("Directors:").font(.callout).fontWeight(.semibold) - Text(fullItem.Directors.joined(separator: ", ")).font(.footnote).lineLimit(1) - .foregroundColor(Color.secondary) - }.padding(.leading, 16).padding(.trailing, 16) - } - if !fullItem.Writers.isEmpty { - HStack { - Text("Writers:").font(.callout).fontWeight(.semibold) - Text(fullItem.Writers.joined(separator: ", ")).font(.footnote).lineLimit(1) - .foregroundColor(Color.secondary) - }.padding(.leading, 16).padding(.trailing, 16) - } - if !fullItem.Studios.isEmpty { - HStack { - Text("Studios:").font(.callout).fontWeight(.semibold) - Text(fullItem.Studios.joined(separator: ", ")).font(.footnote).lineLimit(1) - .foregroundColor(Color.secondary) - }.padding(.leading, 16).padding(.trailing, 16) - } - Spacer().frame(height: 3) + }.padding(.top, -3) + } + if !(item.studios ?? []).isEmpty { + ScrollView(.horizontal, showsIndicators: false) { + HStack { + Text("Studios:").font(.callout).fontWeight(.semibold) + ForEach(item.studios!, id: \.id) { studio in + NavigationLink(destination: LazyView { + LibraryView(withStudio: studio) + }) { + Text(studio.name ?? "").font(.footnote) + } + } + }.padding(.leading, 16).padding(.trailing, 16) } } - } else { - GeometryReader { geometry in - ZStack { - LazyImage(source: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(fullItem.Id)/Images/Backdrop?maxWidth=\(String(Int(geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing)))&quality=80&tag=\(fullItem.Backdrop)")) + Spacer().frame(height: 3) + } + } + } else { + GeometryReader { geometry in + ZStack { + LazyImage(source: item.getBackdropImage(baseURL: globalData.server.baseURI!, maxWidth: Int(geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing))) + .placeholderAndFailure { + Image(uiImage: UIImage(blurHash: item.getBackdropImageBlurHash(), + size: CGSize(width: 16, height: 16))!) + .resizable() + .frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets + .trailing, + height: geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets + .bottom) + } + .contentMode(.aspectFill) + + .opacity(0.3) + .frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing, + height: geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom) + .edgesIgnoringSafeArea(.all) + .blur(radius: 2) + HStack { + VStack { + LazyImage(source: item.getPrimaryImage(baseURL: globalData.server.baseURI!, maxWidth: 120)) .placeholderAndFailure { - Image(uiImage: UIImage(blurHash: fullItem - .BackdropBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : fullItem - .BackdropBlurHash, + Image(uiImage: UIImage(blurHash: item.getPrimaryImageBlurHash(), size: CGSize(width: 16, height: 16))!) .resizable() - .frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets - .trailing, - height: geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets - .bottom) - } - .contentMode(.aspectFill) - - .opacity(0.3) - .frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing, - height: geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom) - .edgesIgnoringSafeArea(.all) - .blur(radius: 2) - HStack { - VStack { - LazyImage(source: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(fullItem.Id)/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)")) - .placeholderAndFailure { - Image(uiImage: UIImage(blurHash: fullItem - .PosterBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : - fullItem.PosterBlurHash, - size: CGSize(width: 16, height: 16))!) - .resizable() - .frame(width: 120, height: 180) - } .frame(width: 120, height: 180) - .cornerRadius(10) - .shadow(radius: 5) - Spacer().frame(height: 15) - Button { - self.playbackInfo.itemToPlay = fullItem - self.playbackInfo.shouldPlay = true - } label: { - HStack { - Text(fullItem.Progress == 0 ? "Play" : "\(progressString) left") - .foregroundColor(Color.white).font(.callout).fontWeight(.semibold) - Image(systemName: "play.fill").foregroundColor(Color.white).font(.system(size: 20)) - } - .frame(width: 120, height: 35) - .background(Color(red: 172 / 255, green: 92 / 255, blue: 195 / 255)) - .cornerRadius(10) - }.buttonStyle(PlainButtonStyle()) - .frame(width: 120, height: 35) - Spacer() } - ScrollView { + .frame(width: 120, height: 180) + .cornerRadius(10) + .shadow(radius: 5) + Spacer().frame(height: 15) + Button { + self.playbackInfo.itemToPlay = item + self.playbackInfo.shouldShowPlayer = true + } label: { + HStack { + Text(item.getItemProgressString() == "" ? "Play" : "\(item.getItemProgressString()) left") + .foregroundColor(Color.white).font(.callout).fontWeight(.semibold) + Image(systemName: "play.fill").foregroundColor(Color.white).font(.system(size: 20)) + } + .frame(width: 120, height: 35) + .background(Color(red: 172 / 255, green: 92 / 255, blue: 195 / 255)) + .cornerRadius(10) + }.buttonStyle(PlainButtonStyle()) + .frame(width: 120, height: 35) + Spacer() + } + ScrollView { + VStack(alignment: .leading) { + HStack { VStack(alignment: .leading) { + Text(item.name ?? "").font(.headline) + .fontWeight(.semibold) + .foregroundColor(.primary) + .fixedSize(horizontal: false, vertical: true) + .offset(x: 14, y: 0) + Spacer().frame(height: 1) HStack { - VStack(alignment: .leading) { - Text(fullItem.Name).font(.headline) + Text(String(item.productionYear ?? 0)).font(.subheadline) + .fontWeight(.medium) + .foregroundColor(.secondary) + .lineLimit(1) + Text(item.getItemRuntime()).font(.subheadline) + .fontWeight(.medium) + .foregroundColor(.secondary) + .lineLimit(1) + if item.officialRating != nil { + Text(item.officialRating!).font(.subheadline) .fontWeight(.semibold) - .foregroundColor(.primary) - .fixedSize(horizontal: false, vertical: true) - .offset(x: 14, y: 0) - Spacer().frame(height: 1) + .foregroundColor(.secondary) + .lineLimit(1) + .padding(EdgeInsets(top: 1, leading: 4, bottom: 1, trailing: 4)) + .overlay(RoundedRectangle(cornerRadius: 2) + .stroke(Color.secondary, lineWidth: 1)) + } + if item.communityRating != nil { HStack { - Text(String(fullItem.ProductionYear)).font(.subheadline) - .fontWeight(.medium) + Image(systemName: "star").foregroundColor(.secondary) + Text(String(item.communityRating!)).font(.subheadline) + .fontWeight(.semibold) .foregroundColor(.secondary) .lineLimit(1) - Text(fullItem.Runtime).font(.subheadline) - .fontWeight(.medium) - .foregroundColor(.secondary) - .lineLimit(1) - if fullItem.OfficialRating != "" { - Text(fullItem.OfficialRating).font(.subheadline) - .fontWeight(.semibold) - .foregroundColor(.secondary) - .lineLimit(1) - .padding(EdgeInsets(top: 1, leading: 4, bottom: 1, trailing: 4)) - .overlay(RoundedRectangle(cornerRadius: 2) - .stroke(Color.secondary, lineWidth: 1)) - } - if fullItem.CommunityRating != "0" { - HStack { - Image(systemName: "star").foregroundColor(.secondary) - Text(fullItem.CommunityRating).font(.subheadline) - .fontWeight(.semibold) - .foregroundColor(.secondary) - .lineLimit(1) - .offset(x: -7, y: 0.7) - } - } - Spacer() - }.frame(maxWidth: .infinity, alignment: .leading) - .offset(x: 14) - }.frame(maxWidth: .infinity, alignment: .leading) + .offset(x: -7, y: 0.7) + } + } Spacer() + }.frame(maxWidth: .infinity, alignment: .leading) + .offset(x: 14) + }.frame(maxWidth: .infinity, alignment: .leading) + Spacer() + HStack { + Button { + favorite.toggle() + } label: { + if !favorite { + Image(systemName: "heart").foregroundColor(Color.primary) + .font(.system(size: 20)) + } else { + Image(systemName: "heart.fill").foregroundColor(Color(UIColor.systemRed)) + .font(.system(size: 20)) + } + } + Button { + watched.toggle() + } label: { + if watched { + Image(systemName: "checkmark.rectangle.fill").foregroundColor(Color.primary) + .font(.system(size: 20)) + } else { + Image(systemName: "xmark.rectangle").foregroundColor(Color.primary) + .font(.system(size: 20)) + } + } + } + }.padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) + if !(item.taglines ?? []).isEmpty { + Text(item.taglines!.first!).font(.body).italic().padding(.top, 3) + .fixedSize(horizontal: false, vertical: true).padding(.leading, 16) + .padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) + } + Text(item.overview ?? "").font(.footnote).padding(.top, 3) + .fixedSize(horizontal: false, vertical: true).padding(.bottom, 3).padding(.leading, 16) + .padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) + if !(item.genreItems ?? []).isEmpty { + ScrollView(.horizontal, showsIndicators: false) { + HStack { + Text("Genres:").font(.callout).fontWeight(.semibold) + ForEach(item.genreItems!, id: \.id) { genre in + NavigationLink(destination: LazyView { + LibraryView(withGenre: genre) + }) { + Text(genre.name ?? "").font(.footnote) + } + } + } + .padding(.leading, 16) + .padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) + } + } + if !(item.people ?? []).isEmpty { + ScrollView(.horizontal, showsIndicators: false) { + VStack { + Spacer().frame(height: 8) HStack { - Button { - favorite.toggle() - } label: { - if !favorite { - Image(systemName: "heart").foregroundColor(Color.primary) - .font(.system(size: 20)) - } else { - Image(systemName: "heart.fill").foregroundColor(Color(UIColor.systemRed)) - .font(.system(size: 20)) - } - } - Button { - watched.toggle() - } label: { - if watched { - Image(systemName: "checkmark.rectangle.fill").foregroundColor(Color.primary) - .font(.system(size: 20)) - } else { - Image(systemName: "xmark.rectangle").foregroundColor(Color.primary) - .font(.system(size: 20)) - } - } - } - }.padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) - if fullItem.Tagline != "" { - Text(fullItem.Tagline).font(.body).italic().padding(.top, 3) - .fixedSize(horizontal: false, vertical: true).padding(.leading, 16) - .padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) - } - Text(fullItem.Overview).font(.footnote).padding(.top, 3) - .fixedSize(horizontal: false, vertical: true).padding(.bottom, 3).padding(.leading, 16) - .padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) - if !fullItem.Genres.isEmpty { - ScrollView(.horizontal, showsIndicators: false) { - HStack { - Text("Genres:").font(.callout).fontWeight(.semibold) - ForEach(fullItem.Genres, id: \.Id) { genre in + Spacer().frame(width: 16) + ForEach(item.people!, id: \.self) { person in + if(person.type! == "Actor") { NavigationLink(destination: LazyView { - LibraryView(viewModel: .init(filter: Filter(genres: [ - genre - .Name, - ])), - title: genre.Name) + LibraryView(withPerson: person) }) { - Text(genre.Name).font(.footnote) - } - } - }.padding(.leading, 16) - .padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) - } - } - if !fullItem.Cast.isEmpty { - ScrollView(.horizontal, showsIndicators: false) { - VStack { - Spacer().frame(height: 8) - HStack { - Spacer().frame(width: 16) - ForEach(fullItem.Cast, id: \.Id) { cast in - NavigationLink(destination: LazyView { - LibraryView(viewModel: .init(filter: Filter(personIds: [ - cast - .Id, - ])), title: cast.Name) - }) { - VStack { - LazyImage(source: cast.Image) - .placeholderAndFailure { - Image(uiImage: UIImage(blurHash: cast - .ImageBlurHash == "" ? - "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : - cast.ImageBlurHash, - size: CGSize(width: 16, - height: 16))!) - .resizable() - .aspectRatio(contentMode: .fill) - .frame(width: 100, height: 100) - .cornerRadius(10) - } - .contentMode(.aspectFill) - .frame(width: 100, height: 100) - .cornerRadius(10) - Text(cast.Name).font(.footnote).fontWeight(.regular).lineLimit(1) - .frame(width: 100).foregroundColor(Color.primary) - if cast.Role != "" { - Text(cast.Role).font(.caption).fontWeight(.medium).lineLimit(1) - .foregroundColor(Color.secondary).frame(width: 100) + VStack { + LazyImage(source: person.getImage(baseURL: globalData.server.baseURI!, maxWidth: 100)) + .placeholderAndFailure { + Image(uiImage: UIImage(blurHash: person.getBlurHash(), + size: CGSize(width: 16, + height: 16))!) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: 100, height: 100) + .cornerRadius(10) } + .contentMode(.aspectFill) + .frame(width: 100, height: 100) + .cornerRadius(10) + Text(person.name ?? "").font(.footnote).fontWeight(.regular).lineLimit(1) + .frame(width: 100).foregroundColor(Color.primary) + if person.role != "" { + Text(person.role!).font(.caption).fontWeight(.medium).lineLimit(1) + .foregroundColor(Color.secondary).frame(width: 100) } } - Spacer().frame(width: 10) } - Spacer().frame(width: 55) + Spacer().frame(width: 10) } } - }.padding(.top, -3) - .padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? -55 : 0) + Spacer().frame(width: UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) + } } - if !fullItem.Directors.isEmpty { - HStack { - Text("Directors:").font(.callout).fontWeight(.semibold) - Text(fullItem.Directors.joined(separator: ", ")).font(.footnote).lineLimit(1) - .foregroundColor(Color.secondary) - }.padding(.leading, 16) - .padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) - } - if !fullItem.Writers.isEmpty { - HStack { - Text("Writers:").font(.callout).fontWeight(.semibold) - Text(fullItem.Writers.joined(separator: ", ")).font(.footnote).lineLimit(1) - .foregroundColor(Color.secondary) - }.padding(.leading, 16) - .padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) - } - if !fullItem.Studios.isEmpty { - HStack { - Text("Studios:").font(.callout).fontWeight(.semibold) - Text(fullItem.Studios.joined(separator: ", ")).font(.footnote).lineLimit(1) - .foregroundColor(Color.secondary) - }.padding(.leading, 16) - .padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) - } - Spacer().frame(height: 195) - }.frame(maxHeight: .infinity) + }.padding(.top, -3) } - }.padding(.top, 16).padding(.leading, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) - .edgesIgnoringSafeArea(.leading) + if !(item.studios ?? []).isEmpty { + ScrollView(.horizontal, showsIndicators: false) { + HStack { + Text("Studios:").font(.callout).fontWeight(.semibold) + ForEach(item.studios!, id: \.id) { studio in + NavigationLink(destination: LazyView { + LibraryView(withStudio: studio) + }) { + Text(studio.name ?? "").font(.footnote) + } + } + } + .padding(.leading, 16) + .padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) + } + } + Spacer().frame(height: 195) + }.frame(maxHeight: .infinity) } - } + }.padding(.top, 16).padding(.leading, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) + .edgesIgnoringSafeArea(.leading) } } } - .navigationBarTitleDisplayMode(.inline) - .navigationTitle(fullItem.Name) - }.onAppear(perform: loadData) - .supportedOrientations(.allButUpsideDown) - .overrideViewPreference(.unspecified) - .preferredColorScheme(.none) - .prefersHomeIndicatorAutoHidden(false) + } + .onAppear(perform: { + favorite = item.userData?.isFavorite ?? false + watched = item.userData?.played ?? false + dump(item) + }) + .navigationBarTitleDisplayMode(.inline) + .navigationTitle(item.name!) + .supportedOrientations(.allButUpsideDown) + .overrideViewPreference(.unspecified) + .preferredColorScheme(.none) + .prefersHomeIndicatorAutoHidden(false) } } diff --git a/JellyfinPlayer/NextUpView.swift b/JellyfinPlayer/NextUpView.swift index 9afd044c..cd04c946 100644 --- a/JellyfinPlayer/NextUpView.swift +++ b/JellyfinPlayer/NextUpView.swift @@ -41,7 +41,7 @@ struct NextUpView: View { LazyHStack() { Spacer().frame(width:16) ForEach(items, id: \.id) { item in - NavigationLink(destination: EmptyView()) { + NavigationLink(destination: ItemView(item: item)) { VStack(alignment: .leading) { LazyImage(source: item.getSeriesPrimaryImage(baseURL: globalData.server.baseURI!, maxWidth: 100)) .placeholderAndFailure { diff --git a/JellyfinPlayer/SeasonItemView.swift b/JellyfinPlayer/SeasonItemView.swift index 65c810b9..796a7c18 100644 --- a/JellyfinPlayer/SeasonItemView.swift +++ b/JellyfinPlayer/SeasonItemView.swift @@ -7,34 +7,22 @@ import NukeUI import SwiftUI -import SwiftyJSON -import SwiftyRequest +import JellyfinAPI struct SeasonItemView: View { - @EnvironmentObject - var globalData: GlobalData - @EnvironmentObject - var orientationInfo: OrientationInfo - @State - private var isLoading: Bool = true + @EnvironmentObject var globalData: GlobalData + @EnvironmentObject var orientationInfo: OrientationInfo + @State private var isLoading: Bool = true + var item: ResumeItem - @State - var fullItem = DetailItem() - @State - var episodes: [DetailItem] = [] - @State - private var progressString: String = "" - @State - private var hasAppearedOnce: Bool = false + + @State private var episodes: [BaseItemDto] = [] init(item: ResumeItem) { self.item = item } - func loadData() { - if hasAppearedOnce { - return - } + func onAppear() { let url = "/Users/\(globalData.user.user_id ?? "")/Items/\(item.Id)" let request = RestRequest(method: .get, url: (globalData.server.baseURI ?? "") + url) @@ -483,7 +471,7 @@ struct SeasonItemView: View { LoadingView(isShowing: $isLoading) { innerBody } - .onAppear(perform: loadData) + .onAppear(perform: onAppear) .navigationBarTitleDisplayMode(.inline) .navigationTitle("\(item.Name) - \(item.SeriesName ?? "")") } diff --git a/JellyfinPlayer/Views/SettingsView.swift b/JellyfinPlayer/SettingsView.swift similarity index 86% rename from JellyfinPlayer/Views/SettingsView.swift rename to JellyfinPlayer/SettingsView.swift index a6cd298d..d5ae9398 100644 --- a/JellyfinPlayer/Views/SettingsView.swift +++ b/JellyfinPlayer/SettingsView.swift @@ -121,3 +121,36 @@ struct SettingsView: View { }.onAppear(perform: onAppear) } } + +struct UserSettings: Decodable { + var LocalMaxBitrate: Int; + var RemoteMaxBitrate: Int; + var AutoSelectSubtitles: Bool; + var AutoSelectSubtitlesLangcode: String; + var SubtitlePositionOffset: Int; + var SubtitleFontName: String; +} + +struct Bitrates: Codable, Hashable { + public var name: String + public var value: Int +} + +final class SettingsViewModel: ObservableObject { + var bitrates: [Bitrates] = [] + + init() { + let url = Bundle.main.url(forResource: "bitrates", withExtension: "json")! + + do { + let jsonData = try Data(contentsOf: url, options: .mappedIfSafe) + do { + self.bitrates = try JSONDecoder().decode([Bitrates].self, from: jsonData) + } catch { + print(error) + } + } catch { + print(error) + } + } +} diff --git a/JellyfinPlayer/Views/VideoPlayer.storyboard b/JellyfinPlayer/VideoPlayer.storyboard similarity index 100% rename from JellyfinPlayer/Views/VideoPlayer.storyboard rename to JellyfinPlayer/VideoPlayer.storyboard diff --git a/JellyfinPlayer/Views/VideoPlayer.swift b/JellyfinPlayer/VideoPlayer.swift similarity index 95% rename from JellyfinPlayer/Views/VideoPlayer.swift rename to JellyfinPlayer/VideoPlayer.swift index e6552e61..a506e0ad 100644 --- a/JellyfinPlayer/Views/VideoPlayer.swift +++ b/JellyfinPlayer/VideoPlayer.swift @@ -291,6 +291,24 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe self.sendPlayReport() playbackItem = item; } + + DispatchQueue.global(qos: .background).async { + mediaPlayer.media = VLCMedia(url: playbackItem.videoUrl) + mediaPlayer.play() + mediaPlayer.jumpForward(Int32(manifest.userData?.playbackPositionTicks ?? 0/10000000)) + mediaPlayer.pause() + subtitleTrackArray.forEach() { sub in + if(sub.id != -1 && sub.delivery == .external && sub.codec != "subrip") { + print("adding subs for id: \(sub.id) w/ url: \(sub.url)") + mediaPlayer.addPlaybackSlave(sub.url, type: .subtitle, enforce: false) + } + } + delegate?.showLoadingView(self) + while(mediaPlayer.numberOfSubtitlesTracks != subtitleTrackArray.count - 1) {} + mediaPlayer.currentVideoSubTitleIndex = selectedCaptionTrack; + mediaPlayer.pause() + mediaPlayer.play() + } }) .store(in: &globalData.pendingAPIRequests) } diff --git a/JellyfinPlayer/Views/VideoPlayerSettingsView.swift b/JellyfinPlayer/VideoPlayerSettingsView.swift similarity index 100% rename from JellyfinPlayer/Views/VideoPlayerSettingsView.swift rename to JellyfinPlayer/VideoPlayerSettingsView.swift diff --git a/JellyfinPlayer/ViewModels/LibraryListViewModel.swift b/JellyfinPlayer/ViewModels/LibraryListViewModel.swift deleted file mode 100644 index 62d724ba..00000000 --- a/JellyfinPlayer/ViewModels/LibraryListViewModel.swift +++ /dev/null @@ -1,38 +0,0 @@ -/* JellyfinPlayer/Swiftfin is subject to the terms of the Mozilla Public - * License, v2.0. If a copy of the MPL was not distributed with this - * file, you can obtain one at https://mozilla.org/MPL/2.0/. - * - * Copyright 2021 Aiden Vigue & Jellyfin Contributors - */ - -import Combine -import CombineMoya -import Foundation -import Moya -import SwiftyJSON - -final class LibraryListViewModel: ObservableObject { - fileprivate var provider = - MoyaProvider() - - @Published - var libraryIDs = [String]() - @Published - var libraryNames = [String: String]() - - fileprivate var cancellables = Set() - - init(libraryNames: [String: String], libraryIDs: [String]) { - self.libraryIDs = libraryIDs - self.libraryNames = libraryNames - refresh() - } - - func refresh() { - libraryIDs.append("favorites") - libraryNames["favorites"] = "Favorites" - - libraryIDs.append("genres") - libraryNames["genres"] = "Genres - WIP" - } -} diff --git a/JellyfinPlayer/ViewModels/LibrarySearchViewModel.swift b/JellyfinPlayer/ViewModels/LibrarySearchViewModel.swift deleted file mode 100644 index 459ca373..00000000 --- a/JellyfinPlayer/ViewModels/LibrarySearchViewModel.swift +++ /dev/null @@ -1,105 +0,0 @@ -/* JellyfinPlayer/Swiftfin is subject to the terms of the Mozilla Public - * License, v2.0. If a copy of the MPL was not distributed with this - * file, you can obtain one at https://mozilla.org/MPL/2.0/. - * - * Copyright 2021 Aiden Vigue & Jellyfin Contributors - */ - -import Combine -import CombineMoya -import Foundation -import Moya -import SwiftyJSON - -final class LibrarySearchViewModel: ObservableObject { - fileprivate var provider = MoyaProvider() - - var filter: Filter - - @Published - var items = [ResumeItem]() - - @Published - var searchQuery = "" - @Published - var isLoading: Bool = true - - var page = 1 - - var globalData = GlobalData() { - didSet { - injectEnvironmentData() - } - } - - fileprivate var cancellables = Set() - - init(filter: Filter) { - self.filter = filter - } - - fileprivate func injectEnvironmentData() { - cancellables.removeAll() - - $searchQuery - .debounce(for: 0.25, scheduler: DispatchQueue.main) - .sink(receiveValue: requestSearch(query:)) - .store(in: &cancellables) - } - - fileprivate func requestSearch(query: String) { - isLoading = true - provider.requestPublisher(.search(globalData: globalData, filter: filter, searchQuery: query, page: page)) - // .map(ResumeItem.self) TO DO - .print() - .sink(receiveCompletion: { [weak self] _ in - guard let self = self else { return } - self.isLoading = false - }, receiveValue: { [weak self] response in - guard let self = self else { return } - let body = response.data - var innerItems = [ResumeItem]() - do { - let json = try JSON(data: body) - for (_, item): (String, JSON) in json["Items"] { - // Do something you want - var itemObj = ResumeItem() - itemObj.Type = item["Type"].string ?? "" - if itemObj.Type == "Series" { - itemObj.ItemBadge = item["UserData"]["UnplayedItemCount"].int ?? 0 - itemObj.Image = item["ImageTags"]["Primary"].string ?? "" - itemObj.ImageType = "Primary" - itemObj.BlurHash = item["ImageBlurHashes"]["Primary"][itemObj.Image].string ?? "" - itemObj.Name = item["Name"].string ?? "" - itemObj.Type = item["Type"].string ?? "" - itemObj.IndexNumber = nil - itemObj.Id = item["Id"].string ?? "" - itemObj.ParentIndexNumber = nil - itemObj.SeasonId = nil - itemObj.SeriesId = nil - itemObj.SeriesName = nil - itemObj.ProductionYear = item["ProductionYear"].int ?? 0 - } else { - itemObj.ProductionYear = item["ProductionYear"].int ?? 0 - itemObj.Image = item["ImageTags"]["Primary"].string ?? "" - itemObj.ImageType = "Primary" - itemObj.BlurHash = item["ImageBlurHashes"]["Primary"][itemObj.Image].string ?? "" - itemObj.Name = item["Name"].string ?? "" - itemObj.Type = item["Type"].string ?? "" - itemObj.IndexNumber = item["IndexNumber"].int ?? nil - itemObj.Id = item["Id"].string ?? "" - itemObj.ParentIndexNumber = item["ParentIndexNumber"].int ?? nil - itemObj.SeasonId = item["SeasonId"].string ?? nil - itemObj.SeriesId = item["SeriesId"].string ?? nil - itemObj.SeriesName = item["SeriesName"].string ?? nil - } - itemObj.Watched = item["UserData"]["Played"].bool ?? false - - innerItems.append(itemObj) - } - } catch {} - self.items = innerItems - }) - .store(in: &cancellables) - } -} diff --git a/JellyfinPlayer/ViewModels/LibraryViewModel.swift b/JellyfinPlayer/ViewModels/LibraryViewModel.swift deleted file mode 100644 index c3285f66..00000000 --- a/JellyfinPlayer/ViewModels/LibraryViewModel.swift +++ /dev/null @@ -1,157 +0,0 @@ -/* JellyfinPlayer/Swiftfin is subject to the terms of the Mozilla Public - * License, v2.0. If a copy of the MPL was not distributed with this - * file, you can obtain one at https://mozilla.org/MPL/2.0/. - * - * Copyright 2021 Aiden Vigue & Jellyfin Contributors - */ - -import Combine -import CombineMoya -import Foundation -import Moya -import SwiftyJSON - -final class LibraryViewModel: ObservableObject { - fileprivate var provider = - MoyaProvider() - - @Published - var filter: Filter - - @Published - var items = [ResumeItem]() - - @Published - var isLoading: Bool = true - - @Published - var isHiddenPreviousButton = true - @Published - var isHiddenNextButton = true - - @Published - var totalPages = 1 - - @Published - var page = 1 - - var globalData = GlobalData() { - didSet { - injectEnvironmentData() - } - } - - fileprivate var cancellables = Set() - - init(filter: Filter = Filter()) { - self.filter = filter - } - - fileprivate func injectEnvironmentData() { - cancellables.removeAll() - - $filter - .sink(receiveValue: requestInitItems(_:)) - .store(in: &cancellables) - } - - func requestNextPage() { - page += 1 - requestItems(filter) - } - - func requestPreviousPage() { - page -= 1 - requestItems(filter) - } - - func requestInitItems(_ filter: Filter) { - page = 1 - requestItems(filter) - } - - fileprivate func requestItems(_ filter: Filter) { - isLoading = true - provider.requestPublisher(.items(globalData: globalData, filter: filter, page: page)) - .receive(on: DispatchQueue.main) - .map { response -> ([ResumeItem], Int) in - let body = response.data - var totalCount = 0 - var innerItems = [ResumeItem]() - do { - let json = try JSON(data: body) - totalCount = json["TotalRecordCount"].int ?? 0 - for (_, item): (String, JSON) in json["Items"] { - // Do something you want - var itemObj = ResumeItem() - itemObj.Type = item["Type"].string ?? "" - if itemObj.Type == "Series" { - itemObj.ItemBadge = item["UserData"]["UnplayedItemCount"].int ?? 0 - itemObj.Image = item["ImageTags"]["Primary"].string ?? "" - itemObj.ImageType = "Primary" - itemObj.BlurHash = item["ImageBlurHashes"]["Primary"][itemObj.Image].string ?? "" - itemObj.Name = item["Name"].string ?? "" - itemObj.Type = item["Type"].string ?? "" - itemObj.IndexNumber = nil - itemObj.Id = item["Id"].string ?? "" - itemObj.ParentIndexNumber = nil - itemObj.SeasonId = nil - itemObj.SeriesId = nil - itemObj.SeriesName = nil - itemObj.ProductionYear = item["ProductionYear"].int ?? 0 - } else { - itemObj.ProductionYear = item["ProductionYear"].int ?? 0 - itemObj.Image = item["ImageTags"]["Primary"].string ?? "" - itemObj.ImageType = "Primary" - itemObj.BlurHash = item["ImageBlurHashes"]["Primary"][itemObj.Image].string ?? "" - itemObj.Name = item["Name"].string ?? "" - itemObj.Type = item["Type"].string ?? "" - itemObj.IndexNumber = item["IndexNumber"].int ?? nil - itemObj.Id = item["Id"].string ?? "" - itemObj.ParentIndexNumber = item["ParentIndexNumber"].int ?? nil - itemObj.SeasonId = item["SeasonId"].string ?? nil - itemObj.SeriesId = item["SeriesId"].string ?? nil - itemObj.SeriesName = item["SeriesName"].string ?? nil - } - itemObj.Watched = item["UserData"]["Played"].bool ?? false - - innerItems.append(itemObj) - } - } catch {} - return (innerItems, totalCount) - } - .sink(receiveCompletion: { [weak self] _ in - guard let self = self else { return } - self.isLoading = false - }, receiveValue: { [weak self] items, count in - guard let self = self else { return } - print(count) - print(items.count) - print(self.page) - - self.totalPages = Int(Float(Double(count)/100.0).rounded(.up)) - - if(count > 100) { - self.isHiddenPreviousButton = true - self.isHiddenNextButton = true - - if(self.page > 1) { - self.isHiddenPreviousButton = false - } - - if(self.page * 100 < count) { - self.isHiddenNextButton = false - } - } else { - self.isHiddenPreviousButton = true - self.isHiddenNextButton = true - } - - print(self.isHiddenPreviousButton) - print(self.isHiddenNextButton) - - self.items = items - }) - .store(in: &cancellables) - } -} diff --git a/JellyfinPlayer/ViewModels/SettingsViewModel.swift b/JellyfinPlayer/ViewModels/SettingsViewModel.swift deleted file mode 100644 index 2af7f237..00000000 --- a/JellyfinPlayer/ViewModels/SettingsViewModel.swift +++ /dev/null @@ -1,27 +0,0 @@ -/* JellyfinPlayer/Swiftfin is subject to the terms of the Mozilla Public - * License, v2.0. If a copy of the MPL was not distributed with this - * file, you can obtain one at https://mozilla.org/MPL/2.0/. - * - * Copyright 2021 Aiden Vigue & Jellyfin Contributors - */ - -import Foundation - -final class SettingsViewModel: ObservableObject { - var bitrates: [Bitrates] = [] - - init() { - let url = Bundle.main.url(forResource: "bitrates", withExtension: "json")! - - do { - let jsonData = try Data(contentsOf: url, options: .mappedIfSafe) - do { - self.bitrates = try JSONDecoder().decode([Bitrates].self, from: jsonData) - } catch { - print(error) - } - } catch { - print(error) - } - } -} diff --git a/Shared/Extensions/APIExtensions.swift b/Shared/Extensions/APIExtensions.swift index 94fea2e1..718c197a 100644 --- a/Shared/Extensions/APIExtensions.swift +++ b/Shared/Extensions/APIExtensions.swift @@ -12,6 +12,8 @@ import UIKit //001fC^ = dark grey plain blurhash extension BaseItemDto { + + //MARK: Images func getSeriesPrimaryImageBlurHash() -> String { let rawImgURL = self.getSeriesPrimaryImage(baseURL: "", maxWidth: 1).absoluteString; let imgTag = rawImgURL.components(separatedBy: "&tag=")[1]; @@ -78,4 +80,50 @@ extension BaseItemDto { let urlString = "\(baseURL)/Items/\(self.id ?? "")/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=85&tag=\(imageTag)" return URL(string: urlString)! } + + //MARK: Calculations + func getItemRuntime() -> String { + let seconds: Int = Int(self.runTimeTicks!) / 10_000_000 + let hours = (seconds / 3600) + let minutes = ((seconds - (hours * 3600)) / 60) + if hours != 0 { + return "\(hours):\(String(minutes).leftPad(toWidth: 2, withString: "0"))" + } else { + return "\(String(minutes).leftPad(toWidth: 2, withString: "0"))m" + } + } + + func getItemProgressString() -> String { + if(self.userData?.playbackPositionTicks == nil || self.userData?.playbackPositionTicks == 0) { + return ""; + } + + let remainingSecs = Int(self.runTimeTicks! - (self.userData?.playbackPositionTicks!)!) / 10_000_000 + let proghours = Int(remainingSecs / 3600) + let progminutes = Int((Int(remainingSecs) - (proghours * 3600)) / 60) + if proghours != 0 { + return "\(proghours):\(String(progminutes).leftPad(toWidth: 2, withString: "0"))" + } else { + return "\(String(progminutes).leftPad(toWidth: 2, withString: "0"))m" + } + } +} + +extension BaseItemPerson { + func getImage(baseURL: String, maxWidth: Int) -> URL { + let imageType = "Primary"; + let imageTag = self.primaryImageTag ?? "" + + let x = UIScreen.main.nativeScale * CGFloat(maxWidth) + + let urlString = "\(baseURL)/Items/\(self.id ?? "")/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=85&tag=\(imageTag)" + return URL(string: urlString)! + } + + func getBlurHash() -> String { + let rawImgURL = self.getImage(baseURL: "", maxWidth: 1).absoluteString; + let imgTag = rawImgURL.components(separatedBy: "&tag=")[1]; + + return self.imageBlurHashes?.primary?[imgTag] ?? "001fC^"; + } }