Start moving to generated client
This commit is contained in:
parent
a7147e7e7c
commit
007930ec06
|
@ -34,13 +34,15 @@
|
||||||
535870A32669D89F00D05A09 /* Model.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 5377CBFF263B596B003A4E83 /* Model.xcdatamodeld */; };
|
535870A32669D89F00D05A09 /* Model.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 5377CBFF263B596B003A4E83 /* Model.xcdatamodeld */; };
|
||||||
535870A52669D8AE00D05A09 /* ParallaxHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6225FCCA2663841E00E067F6 /* ParallaxHeader.swift */; };
|
535870A52669D8AE00D05A09 /* ParallaxHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6225FCCA2663841E00E067F6 /* ParallaxHeader.swift */; };
|
||||||
535870A62669D8AE00D05A09 /* LazyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621338B22660A07800A81A2A /* LazyView.swift */; };
|
535870A62669D8AE00D05A09 /* LazyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621338B22660A07800A81A2A /* LazyView.swift */; };
|
||||||
535870A72669D8AE00D05A09 /* MultiSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53E4E648263F725B00F67C6B /* MultiSelector.swift */; };
|
535870A72669D8AE00D05A09 /* MultiSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53E4E648263F725B00F67C6B /* MultiSelectorView.swift */; };
|
||||||
535870A82669D8AE00D05A09 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621338922660107500A81A2A /* String.swift */; };
|
535870A82669D8AE00D05A09 /* StringExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621338922660107500A81A2A /* StringExtensions.swift */; };
|
||||||
535870A92669D8AE00D05A09 /* LazyImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621C638126676728004216EA /* LazyImage.swift */; };
|
535870A92669D8AE00D05A09 /* NukeExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621C638126676728004216EA /* NukeExtensions.swift */; };
|
||||||
535870AA2669D8AE00D05A09 /* BlurHashDecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5389277B263CC3DB0035E14B /* BlurHashDecode.swift */; };
|
535870AA2669D8AE00D05A09 /* BlurHashDecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5389277B263CC3DB0035E14B /* BlurHashDecode.swift */; };
|
||||||
535870AD2669D8DD00D05A09 /* Typings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535870AC2669D8DD00D05A09 /* Typings.swift */; };
|
535870AD2669D8DD00D05A09 /* Typings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535870AC2669D8DD00D05A09 /* Typings.swift */; };
|
||||||
535BAE9F2649E569005FA86D /* ItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535BAE9E2649E569005FA86D /* ItemView.swift */; };
|
535BAE9F2649E569005FA86D /* ItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535BAE9E2649E569005FA86D /* ItemView.swift */; };
|
||||||
535BAEA5264A151C005FA86D /* VideoPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535BAEA4264A151C005FA86D /* VideoPlayer.swift */; };
|
535BAEA5264A151C005FA86D /* VideoPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535BAEA4264A151C005FA86D /* VideoPlayer.swift */; };
|
||||||
|
5364F455266CA0DC0026ECBA /* APIExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5364F454266CA0DC0026ECBA /* APIExtensions.swift */; };
|
||||||
|
5364F456266CA0DC0026ECBA /* APIExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5364F454266CA0DC0026ECBA /* APIExtensions.swift */; };
|
||||||
5377CBF5263B596A003A4E83 /* JellyfinPlayerApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5377CBF4263B596A003A4E83 /* JellyfinPlayerApp.swift */; };
|
5377CBF5263B596A003A4E83 /* JellyfinPlayerApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5377CBF4263B596A003A4E83 /* JellyfinPlayerApp.swift */; };
|
||||||
5377CBF7263B596A003A4E83 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5377CBF6263B596A003A4E83 /* ContentView.swift */; };
|
5377CBF7263B596A003A4E83 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5377CBF6263B596A003A4E83 /* ContentView.swift */; };
|
||||||
5377CBF9263B596B003A4E83 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5377CBF8263B596B003A4E83 /* Assets.xcassets */; };
|
5377CBF9263B596B003A4E83 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5377CBF8263B596B003A4E83 /* Assets.xcassets */; };
|
||||||
|
@ -50,7 +52,6 @@
|
||||||
5389276E263C25100035E14B /* ContinueWatchingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5389276D263C25100035E14B /* ContinueWatchingView.swift */; };
|
5389276E263C25100035E14B /* ContinueWatchingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5389276D263C25100035E14B /* ContinueWatchingView.swift */; };
|
||||||
53892770263C25230035E14B /* NextUpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5389276F263C25230035E14B /* NextUpView.swift */; };
|
53892770263C25230035E14B /* NextUpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5389276F263C25230035E14B /* NextUpView.swift */; };
|
||||||
53892772263C8C6F0035E14B /* LoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53892771263C8C6F0035E14B /* LoadingView.swift */; };
|
53892772263C8C6F0035E14B /* LoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53892771263C8C6F0035E14B /* LoadingView.swift */; };
|
||||||
53892777263CBB000035E14B /* JellyApiTypings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53892776263CBB000035E14B /* JellyApiTypings.swift */; };
|
|
||||||
5389277A263CBFE70035E14B /* SwiftyJSON in Frameworks */ = {isa = PBXBuildFile; productRef = 53892779263CBFE70035E14B /* SwiftyJSON */; };
|
5389277A263CBFE70035E14B /* SwiftyJSON in Frameworks */ = {isa = PBXBuildFile; productRef = 53892779263CBFE70035E14B /* SwiftyJSON */; };
|
||||||
5389277C263CC3DB0035E14B /* BlurHashDecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5389277B263CC3DB0035E14B /* BlurHashDecode.swift */; };
|
5389277C263CC3DB0035E14B /* BlurHashDecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5389277B263CC3DB0035E14B /* BlurHashDecode.swift */; };
|
||||||
53987CA426572C1300E7EA70 /* SeasonItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53987CA326572C1300E7EA70 /* SeasonItemView.swift */; };
|
53987CA426572C1300E7EA70 /* SeasonItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53987CA326572C1300E7EA70 /* SeasonItemView.swift */; };
|
||||||
|
@ -66,17 +67,17 @@
|
||||||
53D5E3DE264B47EE00BADDC8 /* MobileVLCKit.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 53D5E3DC264B47EE00BADDC8 /* MobileVLCKit.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
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 */; };
|
53DF641E263D9C0600A7CD1A /* LibraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53DF641D263D9C0600A7CD1A /* LibraryView.swift */; };
|
||||||
53E4E647263F6CF100F67C6B /* LibraryFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53E4E646263F6CF100F67C6B /* LibraryFilterView.swift */; };
|
53E4E647263F6CF100F67C6B /* LibraryFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53E4E646263F6CF100F67C6B /* LibraryFilterView.swift */; };
|
||||||
53E4E649263F725B00F67C6B /* MultiSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53E4E648263F725B00F67C6B /* MultiSelector.swift */; };
|
53E4E649263F725B00F67C6B /* MultiSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53E4E648263F725B00F67C6B /* MultiSelectorView.swift */; };
|
||||||
53EE24E6265060780068F029 /* LibrarySearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53EE24E5265060780068F029 /* LibrarySearchView.swift */; };
|
53EE24E6265060780068F029 /* LibrarySearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53EE24E5265060780068F029 /* LibrarySearchView.swift */; };
|
||||||
53F8377D265FF67C00F456B3 /* VideoPlayerSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53F8377C265FF67C00F456B3 /* VideoPlayerSettingsView.swift */; };
|
53F8377D265FF67C00F456B3 /* VideoPlayerSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53F8377C265FF67C00F456B3 /* VideoPlayerSettingsView.swift */; };
|
||||||
53FF7F2A263CF3F500585C35 /* LatestMediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53FF7F29263CF3F500585C35 /* LatestMediaView.swift */; };
|
53FF7F2A263CF3F500585C35 /* LatestMediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53FF7F29263CF3F500585C35 /* LatestMediaView.swift */; };
|
||||||
6213388E265F777C00A81A2A /* LibraryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6213388D265F777C00A81A2A /* LibraryViewModel.swift */; };
|
6213388E265F777C00A81A2A /* LibraryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6213388D265F777C00A81A2A /* LibraryViewModel.swift */; };
|
||||||
62133890265F83A900A81A2A /* LibraryListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6213388F265F83A900A81A2A /* LibraryListView.swift */; };
|
62133890265F83A900A81A2A /* LibraryListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6213388F265F83A900A81A2A /* LibraryListView.swift */; };
|
||||||
621338932660107500A81A2A /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621338922660107500A81A2A /* String.swift */; };
|
621338932660107500A81A2A /* StringExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621338922660107500A81A2A /* StringExtensions.swift */; };
|
||||||
62133895266096EF00A81A2A /* LibraryListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62133894266096EF00A81A2A /* LibraryListViewModel.swift */; };
|
62133895266096EF00A81A2A /* LibraryListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62133894266096EF00A81A2A /* LibraryListViewModel.swift */; };
|
||||||
621338B32660A07800A81A2A /* LazyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621338B22660A07800A81A2A /* LazyView.swift */; };
|
621338B32660A07800A81A2A /* LazyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621338B22660A07800A81A2A /* LazyView.swift */; };
|
||||||
621C638026672A30004216EA /* NukeUI in Frameworks */ = {isa = PBXBuildFile; productRef = 621C637F26672A30004216EA /* NukeUI */; };
|
621C638026672A30004216EA /* NukeUI in Frameworks */ = {isa = PBXBuildFile; productRef = 621C637F26672A30004216EA /* NukeUI */; };
|
||||||
621C638226676728004216EA /* LazyImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621C638126676728004216EA /* LazyImage.swift */; };
|
621C638226676728004216EA /* NukeExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621C638126676728004216EA /* NukeExtensions.swift */; };
|
||||||
6225FCCB2663841E00E067F6 /* ParallaxHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6225FCCA2663841E00E067F6 /* ParallaxHeader.swift */; };
|
6225FCCB2663841E00E067F6 /* ParallaxHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6225FCCA2663841E00E067F6 /* ParallaxHeader.swift */; };
|
||||||
6273DD43265F4195009C1D0B /* Moya in Frameworks */ = {isa = PBXBuildFile; productRef = 6273DD42265F4195009C1D0B /* Moya */; };
|
6273DD43265F4195009C1D0B /* Moya in Frameworks */ = {isa = PBXBuildFile; productRef = 6273DD42265F4195009C1D0B /* Moya */; };
|
||||||
6273DD45265F4195009C1D0B /* CombineMoya in Frameworks */ = {isa = PBXBuildFile; productRef = 6273DD44265F4195009C1D0B /* CombineMoya */; };
|
6273DD45265F4195009C1D0B /* CombineMoya in Frameworks */ = {isa = PBXBuildFile; productRef = 6273DD44265F4195009C1D0B /* CombineMoya */; };
|
||||||
|
@ -138,6 +139,7 @@
|
||||||
535870AC2669D8DD00D05A09 /* Typings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Typings.swift; sourceTree = "<group>"; };
|
535870AC2669D8DD00D05A09 /* Typings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Typings.swift; sourceTree = "<group>"; };
|
||||||
535BAE9E2649E569005FA86D /* ItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemView.swift; sourceTree = "<group>"; };
|
535BAE9E2649E569005FA86D /* ItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemView.swift; sourceTree = "<group>"; };
|
||||||
535BAEA4264A151C005FA86D /* VideoPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayer.swift; sourceTree = "<group>"; };
|
535BAEA4264A151C005FA86D /* VideoPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayer.swift; sourceTree = "<group>"; };
|
||||||
|
5364F454266CA0DC0026ECBA /* APIExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIExtensions.swift; sourceTree = "<group>"; };
|
||||||
5377CBF1263B596A003A4E83 /* JellyfinPlayer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = JellyfinPlayer.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
5377CBF1263B596A003A4E83 /* JellyfinPlayer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = JellyfinPlayer.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
5377CBF4263B596A003A4E83 /* JellyfinPlayerApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinPlayerApp.swift; sourceTree = "<group>"; };
|
5377CBF4263B596A003A4E83 /* JellyfinPlayerApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinPlayerApp.swift; sourceTree = "<group>"; };
|
||||||
5377CBF6263B596A003A4E83 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
5377CBF6263B596A003A4E83 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||||
|
@ -149,7 +151,6 @@
|
||||||
5389276D263C25100035E14B /* ContinueWatchingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContinueWatchingView.swift; sourceTree = "<group>"; };
|
5389276D263C25100035E14B /* ContinueWatchingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContinueWatchingView.swift; sourceTree = "<group>"; };
|
||||||
5389276F263C25230035E14B /* NextUpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextUpView.swift; sourceTree = "<group>"; };
|
5389276F263C25230035E14B /* NextUpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextUpView.swift; sourceTree = "<group>"; };
|
||||||
53892771263C8C6F0035E14B /* LoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingView.swift; sourceTree = "<group>"; };
|
53892771263C8C6F0035E14B /* LoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingView.swift; sourceTree = "<group>"; };
|
||||||
53892776263CBB000035E14B /* JellyApiTypings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyApiTypings.swift; sourceTree = "<group>"; };
|
|
||||||
5389277B263CC3DB0035E14B /* BlurHashDecode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurHashDecode.swift; sourceTree = "<group>"; };
|
5389277B263CC3DB0035E14B /* BlurHashDecode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurHashDecode.swift; sourceTree = "<group>"; };
|
||||||
53987CA326572C1300E7EA70 /* SeasonItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeasonItemView.swift; sourceTree = "<group>"; };
|
53987CA326572C1300E7EA70 /* SeasonItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeasonItemView.swift; sourceTree = "<group>"; };
|
||||||
53987CA526572F0700E7EA70 /* SeriesItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeriesItemView.swift; sourceTree = "<group>"; };
|
53987CA526572F0700E7EA70 /* SeriesItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeriesItemView.swift; sourceTree = "<group>"; };
|
||||||
|
@ -161,16 +162,16 @@
|
||||||
53D5E3DC264B47EE00BADDC8 /* MobileVLCKit.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = MobileVLCKit.xcframework; path = Carthage/Build/MobileVLCKit.xcframework; sourceTree = "<group>"; };
|
53D5E3DC264B47EE00BADDC8 /* MobileVLCKit.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = MobileVLCKit.xcframework; path = Carthage/Build/MobileVLCKit.xcframework; sourceTree = "<group>"; };
|
||||||
53DF641D263D9C0600A7CD1A /* LibraryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryView.swift; sourceTree = "<group>"; };
|
53DF641D263D9C0600A7CD1A /* LibraryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryView.swift; sourceTree = "<group>"; };
|
||||||
53E4E646263F6CF100F67C6B /* LibraryFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryFilterView.swift; sourceTree = "<group>"; };
|
53E4E646263F6CF100F67C6B /* LibraryFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryFilterView.swift; sourceTree = "<group>"; };
|
||||||
53E4E648263F725B00F67C6B /* MultiSelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiSelector.swift; sourceTree = "<group>"; };
|
53E4E648263F725B00F67C6B /* MultiSelectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiSelectorView.swift; sourceTree = "<group>"; };
|
||||||
53EE24E5265060780068F029 /* LibrarySearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibrarySearchView.swift; sourceTree = "<group>"; };
|
53EE24E5265060780068F029 /* LibrarySearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibrarySearchView.swift; sourceTree = "<group>"; };
|
||||||
53F8377C265FF67C00F456B3 /* VideoPlayerSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerSettingsView.swift; sourceTree = "<group>"; };
|
53F8377C265FF67C00F456B3 /* VideoPlayerSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerSettingsView.swift; sourceTree = "<group>"; };
|
||||||
53FF7F29263CF3F500585C35 /* LatestMediaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LatestMediaView.swift; sourceTree = "<group>"; };
|
53FF7F29263CF3F500585C35 /* LatestMediaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LatestMediaView.swift; sourceTree = "<group>"; };
|
||||||
6213388D265F777C00A81A2A /* LibraryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryViewModel.swift; sourceTree = "<group>"; };
|
6213388D265F777C00A81A2A /* LibraryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryViewModel.swift; sourceTree = "<group>"; };
|
||||||
6213388F265F83A900A81A2A /* LibraryListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryListView.swift; sourceTree = "<group>"; };
|
6213388F265F83A900A81A2A /* LibraryListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryListView.swift; sourceTree = "<group>"; };
|
||||||
621338922660107500A81A2A /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = "<group>"; };
|
621338922660107500A81A2A /* StringExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExtensions.swift; sourceTree = "<group>"; };
|
||||||
62133894266096EF00A81A2A /* LibraryListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryListViewModel.swift; sourceTree = "<group>"; };
|
62133894266096EF00A81A2A /* LibraryListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryListViewModel.swift; sourceTree = "<group>"; };
|
||||||
621338B22660A07800A81A2A /* LazyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyView.swift; sourceTree = "<group>"; };
|
621338B22660A07800A81A2A /* LazyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyView.swift; sourceTree = "<group>"; };
|
||||||
621C638126676728004216EA /* LazyImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyImage.swift; sourceTree = "<group>"; };
|
621C638126676728004216EA /* NukeExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NukeExtensions.swift; sourceTree = "<group>"; };
|
||||||
6225FCCA2663841E00E067F6 /* ParallaxHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParallaxHeader.swift; sourceTree = "<group>"; };
|
6225FCCA2663841E00E067F6 /* ParallaxHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParallaxHeader.swift; sourceTree = "<group>"; };
|
||||||
6273DD47265F41B3009C1D0B /* JellyfinAPIOld.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinAPIOld.swift; sourceTree = "<group>"; };
|
6273DD47265F41B3009C1D0B /* JellyfinAPIOld.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinAPIOld.swift; sourceTree = "<group>"; };
|
||||||
6273DD4D265F47B2009C1D0B /* LibrarySearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibrarySearchViewModel.swift; sourceTree = "<group>"; };
|
6273DD4D265F47B2009C1D0B /* LibrarySearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibrarySearchViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
@ -331,12 +332,13 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
5389277B263CC3DB0035E14B /* BlurHashDecode.swift */,
|
5389277B263CC3DB0035E14B /* BlurHashDecode.swift */,
|
||||||
53E4E648263F725B00F67C6B /* MultiSelector.swift */,
|
53E4E648263F725B00F67C6B /* MultiSelectorView.swift */,
|
||||||
621338B22660A07800A81A2A /* LazyView.swift */,
|
621338B22660A07800A81A2A /* LazyView.swift */,
|
||||||
621338922660107500A81A2A /* String.swift */,
|
621338922660107500A81A2A /* StringExtensions.swift */,
|
||||||
6225FCCA2663841E00E067F6 /* ParallaxHeader.swift */,
|
6225FCCA2663841E00E067F6 /* ParallaxHeader.swift */,
|
||||||
621C638126676728004216EA /* LazyImage.swift */,
|
621C638126676728004216EA /* NukeExtensions.swift */,
|
||||||
53C4404D266C75C70049424C /* HandleAPIRequestCompletion.swift */,
|
53C4404D266C75C70049424C /* HandleAPIRequestCompletion.swift */,
|
||||||
|
5364F454266CA0DC0026ECBA /* APIExtensions.swift */,
|
||||||
);
|
);
|
||||||
path = Extensions;
|
path = Extensions;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -344,7 +346,6 @@
|
||||||
6273DD46265F419B009C1D0B /* APIs */ = {
|
6273DD46265F419B009C1D0B /* APIs */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
53892776263CBB000035E14B /* JellyApiTypings.swift */,
|
|
||||||
6273DD47265F41B3009C1D0B /* JellyfinAPIOld.swift */,
|
6273DD47265F41B3009C1D0B /* JellyfinAPIOld.swift */,
|
||||||
);
|
);
|
||||||
path = APIs;
|
path = APIs;
|
||||||
|
@ -529,10 +530,10 @@
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
535870A82669D8AE00D05A09 /* String.swift in Sources */,
|
535870A82669D8AE00D05A09 /* StringExtensions.swift in Sources */,
|
||||||
535870A52669D8AE00D05A09 /* ParallaxHeader.swift in Sources */,
|
535870A52669D8AE00D05A09 /* ParallaxHeader.swift in Sources */,
|
||||||
535870A72669D8AE00D05A09 /* MultiSelector.swift in Sources */,
|
535870A72669D8AE00D05A09 /* MultiSelectorView.swift in Sources */,
|
||||||
535870A92669D8AE00D05A09 /* LazyImage.swift in Sources */,
|
535870A92669D8AE00D05A09 /* NukeExtensions.swift in Sources */,
|
||||||
5358706C2669D21700D05A09 /* Persistence.swift in Sources */,
|
5358706C2669D21700D05A09 /* Persistence.swift in Sources */,
|
||||||
535870AA2669D8AE00D05A09 /* BlurHashDecode.swift in Sources */,
|
535870AA2669D8AE00D05A09 /* BlurHashDecode.swift in Sources */,
|
||||||
535870652669D21600D05A09 /* ContentView.swift in Sources */,
|
535870652669D21600D05A09 /* ContentView.swift in Sources */,
|
||||||
|
@ -540,6 +541,7 @@
|
||||||
53C4404F266C75C70049424C /* HandleAPIRequestCompletion.swift in Sources */,
|
53C4404F266C75C70049424C /* HandleAPIRequestCompletion.swift in Sources */,
|
||||||
5358706F2669D21700D05A09 /* JellyfinPlayer_tvOS.xcdatamodeld in Sources */,
|
5358706F2669D21700D05A09 /* JellyfinPlayer_tvOS.xcdatamodeld in Sources */,
|
||||||
535870632669D21600D05A09 /* JellyfinPlayer_tvOSApp.swift in Sources */,
|
535870632669D21600D05A09 /* JellyfinPlayer_tvOSApp.swift in Sources */,
|
||||||
|
5364F456266CA0DC0026ECBA /* APIExtensions.swift in Sources */,
|
||||||
535870A32669D89F00D05A09 /* Model.xcdatamodeld in Sources */,
|
535870A32669D89F00D05A09 /* Model.xcdatamodeld in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
@ -548,8 +550,9 @@
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
621338932660107500A81A2A /* String.swift in Sources */,
|
5364F455266CA0DC0026ECBA /* APIExtensions.swift in Sources */,
|
||||||
621C638226676728004216EA /* LazyImage.swift in Sources */,
|
621338932660107500A81A2A /* StringExtensions.swift in Sources */,
|
||||||
|
621C638226676728004216EA /* NukeExtensions.swift in Sources */,
|
||||||
53FF7F2A263CF3F500585C35 /* LatestMediaView.swift in Sources */,
|
53FF7F2A263CF3F500585C35 /* LatestMediaView.swift in Sources */,
|
||||||
5377CBFE263B596B003A4E83 /* PersistenceController.swift in Sources */,
|
5377CBFE263B596B003A4E83 /* PersistenceController.swift in Sources */,
|
||||||
5389276E263C25100035E14B /* ContinueWatchingView.swift in Sources */,
|
5389276E263C25100035E14B /* ContinueWatchingView.swift in Sources */,
|
||||||
|
@ -565,7 +568,7 @@
|
||||||
5377CC01263B596B003A4E83 /* Model.xcdatamodeld in Sources */,
|
5377CC01263B596B003A4E83 /* Model.xcdatamodeld in Sources */,
|
||||||
53DF641E263D9C0600A7CD1A /* LibraryView.swift in Sources */,
|
53DF641E263D9C0600A7CD1A /* LibraryView.swift in Sources */,
|
||||||
53A089D0264DA9DA00D57806 /* MovieItemView.swift in Sources */,
|
53A089D0264DA9DA00D57806 /* MovieItemView.swift in Sources */,
|
||||||
53E4E649263F725B00F67C6B /* MultiSelector.swift in Sources */,
|
53E4E649263F725B00F67C6B /* MultiSelectorView.swift in Sources */,
|
||||||
53E4E647263F6CF100F67C6B /* LibraryFilterView.swift in Sources */,
|
53E4E647263F6CF100F67C6B /* LibraryFilterView.swift in Sources */,
|
||||||
6213388E265F777C00A81A2A /* LibraryViewModel.swift in Sources */,
|
6213388E265F777C00A81A2A /* LibraryViewModel.swift in Sources */,
|
||||||
621338B32660A07800A81A2A /* LazyView.swift in Sources */,
|
621338B32660A07800A81A2A /* LazyView.swift in Sources */,
|
||||||
|
@ -573,7 +576,6 @@
|
||||||
535870AD2669D8DD00D05A09 /* Typings.swift in Sources */,
|
535870AD2669D8DD00D05A09 /* Typings.swift in Sources */,
|
||||||
6273DD48265F41B3009C1D0B /* JellyfinAPIOld.swift in Sources */,
|
6273DD48265F41B3009C1D0B /* JellyfinAPIOld.swift in Sources */,
|
||||||
53C4404E266C75C70049424C /* HandleAPIRequestCompletion.swift in Sources */,
|
53C4404E266C75C70049424C /* HandleAPIRequestCompletion.swift in Sources */,
|
||||||
53892777263CBB000035E14B /* JellyApiTypings.swift in Sources */,
|
|
||||||
5377CBF7263B596A003A4E83 /* ContentView.swift in Sources */,
|
5377CBF7263B596A003A4E83 /* ContentView.swift in Sources */,
|
||||||
53987CA82657424A00E7EA70 /* EpisodeItemView.swift in Sources */,
|
53987CA82657424A00E7EA70 /* EpisodeItemView.swift in Sources */,
|
||||||
5389277C263CC3DB0035E14B /* BlurHashDecode.swift in Sources */,
|
5389277C263CC3DB0035E14B /* BlurHashDecode.swift in Sources */,
|
||||||
|
|
|
@ -1,79 +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 SwiftUI
|
|
||||||
|
|
||||||
extension View {
|
|
||||||
func rectReader(_ binding: Binding<CGRect>, in space: CoordinateSpace) -> some View {
|
|
||||||
self.background(GeometryReader { (geometry) -> AnyView in
|
|
||||||
let rect = geometry.frame(in: space)
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
binding.wrappedValue = rect
|
|
||||||
}
|
|
||||||
return AnyView(Rectangle().fill(Color.clear))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension View {
|
|
||||||
func ifVisible(in rect: CGRect, in space: CoordinateSpace, execute: @escaping (CGRect) -> Void) -> some View {
|
|
||||||
self.background(GeometryReader { (geometry) -> AnyView in
|
|
||||||
let frame = geometry.frame(in: space)
|
|
||||||
if frame.intersects(rect) {
|
|
||||||
execute(frame)
|
|
||||||
}
|
|
||||||
return AnyView(Rectangle().fill(Color.clear))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ServerPublicInfoResponse: Codable {
|
|
||||||
var LocalAddress: String
|
|
||||||
var ServerName: String
|
|
||||||
var Version: String
|
|
||||||
var ProductName: String
|
|
||||||
var OperatingSystem: String
|
|
||||||
var Id: String
|
|
||||||
var StartupWizardCompleted: Bool
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ServerUserResponse: Codable {
|
|
||||||
var Name: String
|
|
||||||
var Id: String
|
|
||||||
var PrimaryImageTag: String
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ServerAuthByNameResponse: Codable {
|
|
||||||
var User: ServerUserResponse
|
|
||||||
var AccessToken: String
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ResumeItem {
|
|
||||||
var Name: String = "";
|
|
||||||
var Id: String = "";
|
|
||||||
var IndexNumber: Int? = nil;
|
|
||||||
var ParentIndexNumber: Int? = nil;
|
|
||||||
var Image: String = "";
|
|
||||||
var ImageType: String = "";
|
|
||||||
var BlurHash: String = "";
|
|
||||||
var `Type`: String = "";
|
|
||||||
var SeasonId: String? = nil;
|
|
||||||
var SeriesId: String? = nil;
|
|
||||||
var SeriesName: String? = nil;
|
|
||||||
var ItemProgress: Double = 0;
|
|
||||||
var SeasonImage: String? = nil;
|
|
||||||
var SeasonImageType: String? = nil;
|
|
||||||
var SeasonImageBlurHash: String? = nil;
|
|
||||||
var ItemBadge: Int? = 0;
|
|
||||||
var ProductionYear: Int = 1999;
|
|
||||||
var Watched: Bool = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ServerMeResponse: Codable {
|
|
||||||
|
|
||||||
}
|
|
|
@ -88,7 +88,7 @@ extension JellyfinAPIOld: TargetType {
|
||||||
switch self {
|
switch self {
|
||||||
case let .items(global, _, _),
|
case let .items(global, _, _),
|
||||||
let .search(global, _, _, _):
|
let .search(global, _, _, _):
|
||||||
return URL(string: global.server?.baseURI ?? "")!
|
return URL(string: global.server.baseURI ?? "")!
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,7 +96,7 @@ extension JellyfinAPIOld: TargetType {
|
||||||
switch self {
|
switch self {
|
||||||
case let .items(global, _, _),
|
case let .items(global, _, _),
|
||||||
let .search(global, _, _, _):
|
let .search(global, _, _, _):
|
||||||
return "/Users/\(global.user?.user_id ?? "")/Items"
|
return "/Users/\(global.user.user_id ?? "")/Items"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,26 +5,19 @@
|
||||||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import SwiftUI
|
//MARK: refactor this file! it's the first swift file I ever wrote and it clearly shows.
|
||||||
|
|
||||||
import SwiftyRequest
|
import SwiftUI
|
||||||
import SwiftyJSON
|
|
||||||
import CoreData
|
import CoreData
|
||||||
import KeychainSwift
|
import KeychainSwift
|
||||||
import NukeUI
|
import NukeUI
|
||||||
import JellyfinAPI
|
import JellyfinAPI
|
||||||
|
|
||||||
class publicUser: ObservableObject {
|
|
||||||
@Published var username: String = "";
|
|
||||||
@Published var hasPassword: Bool = true;
|
|
||||||
@Published var primaryImageTag: String = "";
|
|
||||||
@Published var id: String = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ConnectToServerView: View {
|
struct ConnectToServerView: View {
|
||||||
@Environment(\.managedObjectContext) private var viewContext
|
@Environment(\.managedObjectContext) private var viewContext
|
||||||
@EnvironmentObject var globalData: GlobalData
|
@EnvironmentObject var globalData: GlobalData
|
||||||
@EnvironmentObject var jsi: justSignedIn
|
@EnvironmentObject var jsi: justSignedIn
|
||||||
|
|
||||||
@State private var uri = "";
|
@State private var uri = "";
|
||||||
@State private var isWorking = false;
|
@State private var isWorking = false;
|
||||||
@State private var isErrored = false;
|
@State private var isErrored = false;
|
||||||
|
@ -33,27 +26,26 @@ struct ConnectToServerView: View {
|
||||||
@State private var isConnected = false;
|
@State private var isConnected = false;
|
||||||
@State private var serverName = "";
|
@State private var serverName = "";
|
||||||
@State private var usernameDisabled: Bool = false;
|
@State private var usernameDisabled: Bool = false;
|
||||||
@State private var publicUsers: [publicUser] = [];
|
@State private var publicUsers: [UserDto] = [];
|
||||||
@State private var lastPublicUsers: [publicUser] = [];
|
@State private var lastPublicUsers: [UserDto] = [];
|
||||||
@Binding var rootIsActive : Bool
|
|
||||||
|
|
||||||
let userUUID = UUID();
|
|
||||||
|
|
||||||
@State private var username = "";
|
@State private var username = "";
|
||||||
@State private var password = "";
|
@State private var password = "";
|
||||||
@State private var server_id = "";
|
@State private var server_id = "";
|
||||||
|
|
||||||
@State private var serverSkipped: Bool = false;
|
@State private var serverSkipped: Bool = false;
|
||||||
@State private var serverSkippedAlert: Bool = false;
|
@State private var serverSkippedAlert: Bool = false;
|
||||||
private var reauthDeviceID: String = "";
|
@State private var skip_server_bool: Bool = false;
|
||||||
private var skip_server_bool: Bool = false;
|
@State private var skip_server_obj: Server = Server();
|
||||||
private var skip_server_obj: Server?
|
|
||||||
|
|
||||||
init(skip_server: Bool, skip_server_prefill: Server?, reauth_deviceId: String, isActive: Binding<Bool>) {
|
@Binding var rootIsActive: Bool
|
||||||
|
|
||||||
|
private var reauthDeviceID: String = "";
|
||||||
|
private let userUUID = UUID();
|
||||||
|
|
||||||
|
init(skip_server: Bool, skip_server_prefill: Server, reauth_deviceId: String, isActive: Binding<Bool>) {
|
||||||
|
_rootIsActive = isActive
|
||||||
skip_server_bool = skip_server
|
skip_server_bool = skip_server
|
||||||
skip_server_obj = skip_server_prefill
|
skip_server_obj = skip_server_prefill
|
||||||
reauthDeviceID = reauth_deviceId
|
reauthDeviceID = reauth_deviceId
|
||||||
_rootIsActive = isActive
|
|
||||||
}
|
}
|
||||||
|
|
||||||
init(isActive: Binding<Bool>) {
|
init(isActive: Binding<Bool>) {
|
||||||
|
@ -62,111 +54,91 @@ struct ConnectToServerView: View {
|
||||||
|
|
||||||
func start() {
|
func start() {
|
||||||
if(skip_server_bool) {
|
if(skip_server_bool) {
|
||||||
_uri.wrappedValue = skip_server_obj?.baseURI ?? ""
|
uri = skip_server_obj.baseURI!
|
||||||
let request = RestRequest(method: .get, url: uri + "/users/public")
|
|
||||||
request.responseData() { (result: Result<RestResponse<Data>, RestError>) in
|
UserAPI.getPublicUsers()
|
||||||
switch result {
|
.sink(receiveCompletion: { completion in
|
||||||
case .success(let response):
|
switch completion {
|
||||||
do {
|
case .finished:
|
||||||
let body = response.body;
|
break
|
||||||
let json = try JSON(data: body);
|
case .failure(_):
|
||||||
|
skip_server_bool = false;
|
||||||
for (_,publicUserDto):(String, JSON) in json {
|
skip_server_obj = Server();
|
||||||
let newPublicUser = publicUser()
|
break
|
||||||
newPublicUser.username = publicUserDto["Name"].string ?? ""
|
|
||||||
newPublicUser.hasPassword = publicUserDto["HasPassword"].bool ?? true
|
|
||||||
newPublicUser.primaryImageTag = publicUserDto["PrimaryImageTag"].string ?? ""
|
|
||||||
newPublicUser.id = publicUserDto["Id"].string ?? ""
|
|
||||||
_publicUsers.wrappedValue.append(newPublicUser)
|
|
||||||
}
|
|
||||||
} catch(_) {
|
|
||||||
|
|
||||||
}
|
}
|
||||||
_serverSkipped.wrappedValue = true;
|
}, receiveValue: { response in
|
||||||
_serverSkippedAlert.wrappedValue = true;
|
publicUsers = response
|
||||||
_server_id.wrappedValue = skip_server_obj?.server_id ?? ""
|
|
||||||
_serverName.wrappedValue = skip_server_obj?.name ?? ""
|
serverSkipped = true;
|
||||||
_isConnected.wrappedValue = true;
|
serverSkippedAlert = true;
|
||||||
break
|
server_id = skip_server_obj.server_id!
|
||||||
case .failure(_):
|
serverName = skip_server_obj.name!
|
||||||
_serverSkipped.wrappedValue = true;
|
isConnected = true;
|
||||||
_serverSkippedAlert.wrappedValue = true;
|
})
|
||||||
_server_id.wrappedValue = skip_server_obj?.server_id ?? ""
|
.store(in: &globalData.pendingAPIRequests)
|
||||||
_serverName.wrappedValue = skip_server_obj?.name ?? ""
|
|
||||||
_isConnected.wrappedValue = true;
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func doLogin() {
|
func doLogin() {
|
||||||
_isWorking.wrappedValue = true
|
isWorking = true
|
||||||
let authJson: [String: Any] = ["Username": _username.wrappedValue, "Pw": _password.wrappedValue]
|
|
||||||
let request = RestRequest(method: .post, url: uri + "/Users/authenticatebyname")
|
|
||||||
|
|
||||||
let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String;
|
let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String;
|
||||||
let authHeader = "MediaBrowser Client=\"SwiftFin\", Device=\"\(UIDevice.current.name)\", DeviceId=\"\(serverSkipped ? reauthDeviceID : userUUID.uuidString)\", Version=\"\(appVersion ?? "0.0.1")\"";
|
var deviceName = UIDevice.current.name;
|
||||||
|
deviceName = deviceName.folding(options: .diacriticInsensitive, locale: .current)
|
||||||
|
deviceName = deviceName.removeRegexMatches(pattern: "[^\\w\\s]");
|
||||||
|
|
||||||
request.headerParameters["X-Emby-Authorization"] = authHeader
|
let authHeader = "MediaBrowser Client=\"SwiftFin\", Device=\"\(deviceName)\", DeviceId=\"\(serverSkipped ? reauthDeviceID : userUUID.uuidString)\", Version=\"\(appVersion ?? "0.0.1")\"";
|
||||||
request.contentType = "application/json"
|
|
||||||
request.acceptType = "application/json"
|
|
||||||
request.messageBodyDictionary = authJson
|
|
||||||
|
|
||||||
request.responseData() { (result: Result<RestResponse<Data>, RestError>) in
|
JellyfinAPI.customHeaders["X-Emby-Authorization"] = authHeader
|
||||||
switch result {
|
|
||||||
case .success(let response):
|
UserAPI.authenticateUser(userId: username, pw: password)
|
||||||
|
.sink(receiveCompletion: { completion in
|
||||||
|
isWorking = false
|
||||||
|
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
|
||||||
|
}, receiveValue: { response in
|
||||||
|
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "Server")
|
||||||
|
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let json = try JSON(data: response.body)
|
try viewContext.execute(deleteRequest)
|
||||||
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "Server")
|
} catch _ as NSError {
|
||||||
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
|
|
||||||
|
|
||||||
do {
|
}
|
||||||
try viewContext.execute(deleteRequest)
|
|
||||||
} catch _ as NSError {
|
let fetchRequest2: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "SignedInUser")
|
||||||
// TODO: handle the error
|
let deleteRequest2 = NSBatchDeleteRequest(fetchRequest: fetchRequest2)
|
||||||
}
|
|
||||||
|
|
||||||
let fetchRequest2: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "SignedInUser")
|
|
||||||
let deleteRequest2 = NSBatchDeleteRequest(fetchRequest: fetchRequest2)
|
|
||||||
|
|
||||||
do {
|
do {
|
||||||
try viewContext.execute(deleteRequest2)
|
try viewContext.execute(deleteRequest2)
|
||||||
} catch _ as NSError {
|
} catch _ as NSError {
|
||||||
// TODO: handle the error
|
|
||||||
}
|
|
||||||
|
|
||||||
let newServer = Server(context: viewContext)
|
|
||||||
newServer.baseURI = _uri.wrappedValue
|
|
||||||
newServer.name = _serverName.wrappedValue
|
|
||||||
newServer.server_id = _server_id.wrappedValue
|
|
||||||
|
|
||||||
let newUser = SignedInUser(context: viewContext)
|
|
||||||
newUser.device_uuid = userUUID.uuidString
|
|
||||||
newUser.username = _username.wrappedValue
|
|
||||||
newUser.user_id = json["User"]["Id"].string ?? ""
|
|
||||||
|
|
||||||
let keychain = KeychainSwift()
|
|
||||||
keychain.set(json["AccessToken"].string ?? "", forKey: "AccessToken_\(json["User"]["Id"].string ?? "")")
|
|
||||||
|
|
||||||
do {
|
|
||||||
try viewContext.save()
|
|
||||||
DispatchQueue.main.async { [self] in
|
|
||||||
globalData.authHeader = authHeader
|
|
||||||
_rootIsActive.wrappedValue = false
|
|
||||||
jsi.did = true
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
|
|
||||||
}
|
}
|
||||||
case .failure(_):
|
|
||||||
_isSignInErrored.wrappedValue = true;
|
let newServer = Server(context: viewContext)
|
||||||
}
|
newServer.baseURI = uri
|
||||||
_isWorking.wrappedValue = false;
|
newServer.name = serverName
|
||||||
}
|
newServer.server_id = server_id
|
||||||
|
|
||||||
|
let newUser = SignedInUser(context: viewContext)
|
||||||
|
newUser.device_uuid = userUUID.uuidString
|
||||||
|
newUser.username = username
|
||||||
|
newUser.user_id = response.user!.id!
|
||||||
|
|
||||||
|
let keychain = KeychainSwift()
|
||||||
|
keychain.set(response.accessToken!, forKey: "AccessToken_\(newUser.user_id!)")
|
||||||
|
|
||||||
|
do {
|
||||||
|
try viewContext.save()
|
||||||
|
DispatchQueue.main.async { [self] in
|
||||||
|
globalData.authHeader = authHeader
|
||||||
|
_rootIsActive.wrappedValue = false
|
||||||
|
jsi.did = true
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
print("Couldn't store objects to CoreData")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.store(in: &globalData.pendingAPIRequests)
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
@ -177,56 +149,50 @@ struct ConnectToServerView: View {
|
||||||
.disableAutocorrection(true)
|
.disableAutocorrection(true)
|
||||||
.autocapitalization(.none)
|
.autocapitalization(.none)
|
||||||
Button {
|
Button {
|
||||||
_isWorking.wrappedValue = true;
|
isWorking = true;
|
||||||
if(!_uri.wrappedValue.contains("http")) {
|
if(!uri.contains("http")) {
|
||||||
_uri.wrappedValue = "http://" + _uri.wrappedValue;
|
uri = "http://" + uri;
|
||||||
}
|
}
|
||||||
if(_uri.wrappedValue.last == "/") {
|
if(uri.last == "/") {
|
||||||
_uri.wrappedValue = String(_uri.wrappedValue.dropLast())
|
uri = String(uri.dropLast())
|
||||||
}
|
}
|
||||||
let request = RestRequest(method: .get, url: uri + "/System/Info/Public")
|
|
||||||
request.responseObject() { (result: Result<RestResponse<ServerPublicInfoResponse>, RestError>) in
|
JellyfinAPI.basePath = uri
|
||||||
switch result {
|
SystemAPI.getPublicSystemInfo()
|
||||||
case .success(let response):
|
.sink(receiveCompletion: { completion in
|
||||||
let server = response.body
|
switch completion {
|
||||||
_serverName.wrappedValue = server.ServerName
|
case .finished:
|
||||||
_server_id.wrappedValue = server.Id
|
|
||||||
if(server.StartupWizardCompleted) {
|
|
||||||
_isConnected.wrappedValue = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
let request2 = RestRequest(method: .get, url: uri + "/users/public")
|
|
||||||
request2.responseData() { (result: Result<RestResponse<Data>, RestError>) in
|
|
||||||
switch result {
|
|
||||||
case .success(let response):
|
|
||||||
do {
|
|
||||||
let body = response.body;
|
|
||||||
let json = try JSON(data: body);
|
|
||||||
|
|
||||||
for (_,publicUserDto):(String, JSON) in json {
|
|
||||||
let newPublicUser = publicUser()
|
|
||||||
newPublicUser.username = publicUserDto["Name"].string ?? ""
|
|
||||||
newPublicUser.hasPassword = publicUserDto["HasPassword"].bool ?? true
|
|
||||||
newPublicUser.primaryImageTag = publicUserDto["PrimaryImageTag"].string ?? ""
|
|
||||||
newPublicUser.id = publicUserDto["Id"].string ?? ""
|
|
||||||
_publicUsers.wrappedValue.append(newPublicUser)
|
|
||||||
}
|
|
||||||
} catch(_) {
|
|
||||||
|
|
||||||
}
|
|
||||||
_isWorking.wrappedValue = false;
|
|
||||||
break
|
break
|
||||||
case .failure(_):
|
case .failure(_):
|
||||||
_isErrored.wrappedValue = true;
|
isErrored = true
|
||||||
_isWorking.wrappedValue = false;
|
isWorking = false
|
||||||
break
|
break
|
||||||
}
|
|
||||||
}
|
}
|
||||||
case .failure(_):
|
}, receiveValue: { response in
|
||||||
_isErrored.wrappedValue = true;
|
let server = response
|
||||||
_isWorking.wrappedValue = false;
|
serverName = server.serverName!
|
||||||
}
|
server_id = server.id!
|
||||||
}
|
if(server.startupWizardCompleted!) {
|
||||||
|
isConnected = true;
|
||||||
|
|
||||||
|
UserAPI.getPublicUsers()
|
||||||
|
.sink(receiveCompletion: { completion in
|
||||||
|
switch completion {
|
||||||
|
case .finished:
|
||||||
|
break
|
||||||
|
case .failure(_):
|
||||||
|
isErrored = true
|
||||||
|
isWorking = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}, receiveValue: { response in
|
||||||
|
publicUsers = response
|
||||||
|
isWorking = false
|
||||||
|
})
|
||||||
|
.store(in: &globalData.pendingAPIRequests)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.store(in: &globalData.pendingAPIRequests)
|
||||||
} label: {
|
} label: {
|
||||||
HStack {
|
HStack {
|
||||||
Text("Connect")
|
Text("Connect")
|
||||||
|
@ -240,7 +206,7 @@ struct ConnectToServerView: View {
|
||||||
Alert(title: Text("Error"), message: Text("Couldn't connect to server"), dismissButton: .default(Text("Try again")))
|
Alert(title: Text("Error"), message: Text("Couldn't connect to server"), dismissButton: .default(Text("Try again")))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if(_publicUsers.wrappedValue.count == 0) {
|
if(publicUsers.count == 0) {
|
||||||
Section(header: Text("\(serverSkipped ? "Reauthenticate" : "Login") to \(serverName)")) {
|
Section(header: Text("\(serverSkipped ? "Reauthenticate" : "Login") to \(serverName)")) {
|
||||||
TextField("Username", text: $username)
|
TextField("Username", text: $username)
|
||||||
.disableAutocorrection(true)
|
.disableAutocorrection(true)
|
||||||
|
@ -268,11 +234,11 @@ struct ConnectToServerView: View {
|
||||||
if(serverSkipped) {
|
if(serverSkipped) {
|
||||||
Section() {
|
Section() {
|
||||||
Button {
|
Button {
|
||||||
_serverSkippedAlert.wrappedValue = false;
|
serverSkippedAlert = false
|
||||||
_server_id.wrappedValue = ""
|
server_id = ""
|
||||||
_serverName.wrappedValue = ""
|
serverName = ""
|
||||||
_isConnected.wrappedValue = false;
|
isConnected = false
|
||||||
_serverSkipped.wrappedValue = false;
|
serverSkipped = false
|
||||||
} label: {
|
} label: {
|
||||||
HStack() {
|
HStack() {
|
||||||
HStack() {
|
HStack() {
|
||||||
|
@ -286,8 +252,8 @@ struct ConnectToServerView: View {
|
||||||
} else {
|
} else {
|
||||||
Section() {
|
Section() {
|
||||||
Button {
|
Button {
|
||||||
_publicUsers.wrappedValue = _lastPublicUsers.wrappedValue
|
publicUsers = lastPublicUsers
|
||||||
_usernameDisabled.wrappedValue = false;
|
usernameDisabled = false;
|
||||||
} label: {
|
} label: {
|
||||||
HStack() {
|
HStack() {
|
||||||
HStack() {
|
HStack() {
|
||||||
|
@ -301,26 +267,26 @@ struct ConnectToServerView: View {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Section(header: Text("\(serverSkipped ? "Reauthenticate" : "Login") to \(serverName)")) {
|
Section(header: Text("\(serverSkipped ? "Reauthenticate" : "Login") to \(serverName)")) {
|
||||||
ForEach(publicUsers, id: \.id) { pubuser in
|
ForEach(publicUsers, id: \.id) { publicUser in
|
||||||
HStack() {
|
HStack() {
|
||||||
Button() {
|
Button() {
|
||||||
if(pubuser.hasPassword) {
|
if(publicUser.hasPassword!) {
|
||||||
_lastPublicUsers.wrappedValue = _publicUsers.wrappedValue
|
lastPublicUsers = publicUsers
|
||||||
_username.wrappedValue = pubuser.username
|
username = publicUser.name!
|
||||||
_usernameDisabled.wrappedValue = true;
|
usernameDisabled = true
|
||||||
_publicUsers.wrappedValue = []
|
publicUsers = []
|
||||||
} else {
|
} else {
|
||||||
_publicUsers.wrappedValue = []
|
publicUsers = []
|
||||||
_password.wrappedValue = "";
|
password = ""
|
||||||
_username.wrappedValue = pubuser.username
|
username = publicUser.name!
|
||||||
doLogin()
|
doLogin()
|
||||||
}
|
}
|
||||||
} label: {
|
} label: {
|
||||||
HStack() {
|
HStack() {
|
||||||
Text(pubuser.username).font(.subheadline).fontWeight(.semibold)
|
Text(publicUser.name!).font(.subheadline).fontWeight(.semibold)
|
||||||
Spacer()
|
Spacer()
|
||||||
if(pubuser.primaryImageTag != "") {
|
if(publicUser.primaryImageTag != "") {
|
||||||
LazyImage(source: URL(string: "\(uri)/Users/\(pubuser.id)/Images/Primary?width=200&quality=80&tag=\(pubuser.primaryImageTag)"))
|
LazyImage(source: URL(string: "\(uri)/Users/\(publicUser.id!)/Images/Primary?width=200&quality=80&tag=\(publicUser.primaryImageTag!)"))
|
||||||
.contentMode(.aspectFill)
|
.contentMode(.aspectFill)
|
||||||
.frame(width: 60, height: 60)
|
.frame(width: 60, height: 60)
|
||||||
.cornerRadius(30.0)
|
.cornerRadius(30.0)
|
||||||
|
@ -342,9 +308,9 @@ struct ConnectToServerView: View {
|
||||||
|
|
||||||
Section() {
|
Section() {
|
||||||
Button() {
|
Button() {
|
||||||
_lastPublicUsers.wrappedValue = _publicUsers.wrappedValue;
|
lastPublicUsers = publicUsers
|
||||||
_publicUsers.wrappedValue = []
|
publicUsers = []
|
||||||
_username.wrappedValue = ""
|
username = ""
|
||||||
} label: {
|
} label: {
|
||||||
HStack() {
|
HStack() {
|
||||||
Text("Other User").font(.subheadline).fontWeight(.semibold)
|
Text("Other User").font(.subheadline).fontWeight(.semibold)
|
||||||
|
|
|
@ -8,51 +8,40 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
import KeychainSwift
|
import KeychainSwift
|
||||||
import SwiftyJSON
|
|
||||||
import SwiftyRequest
|
|
||||||
import Nuke
|
import Nuke
|
||||||
import Combine
|
import Combine
|
||||||
import JellyfinAPI
|
import JellyfinAPI
|
||||||
|
|
||||||
struct ContentView: View {
|
struct ContentView: View {
|
||||||
@Environment(\.managedObjectContext)
|
@Environment(\.managedObjectContext) private var viewContext
|
||||||
private var viewContext
|
@EnvironmentObject var orientationInfo: OrientationInfo
|
||||||
@EnvironmentObject
|
@EnvironmentObject var jsi: justSignedIn
|
||||||
var orientationInfo: OrientationInfo
|
|
||||||
@StateObject
|
@StateObject private var globalData = GlobalData()
|
||||||
private var globalData = GlobalData()
|
|
||||||
@EnvironmentObject
|
|
||||||
var jsi: justSignedIn
|
|
||||||
|
|
||||||
@FetchRequest(entity: Server.entity(),
|
@FetchRequest(entity: Server.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Server.name, ascending: true)])
|
||||||
sortDescriptors: [NSSortDescriptor(keyPath: \Server.name, ascending: true)])
|
private var servers: FetchedResults<Server>
|
||||||
private var servers: FetchedResults<Server>
|
|
||||||
|
|
||||||
@FetchRequest(entity: SignedInUser.entity(),
|
@FetchRequest(entity: SignedInUser.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \SignedInUser.username, ascending: true)])
|
||||||
sortDescriptors: [NSSortDescriptor(keyPath: \SignedInUser.username,
|
private var savedUsers: FetchedResults<SignedInUser>
|
||||||
ascending: true)])
|
|
||||||
private var savedUsers: FetchedResults<SignedInUser>
|
|
||||||
|
|
||||||
@State
|
@State private var needsToSelectServer = false
|
||||||
private var needsToSelectServer = false
|
@State private var isLoading = false
|
||||||
@State
|
@State private var tabSelection: String = "Home"
|
||||||
private var isLoading = false
|
@State private var libraries: [String] = []
|
||||||
@State
|
@State private var library_names: [String: String] = [:]
|
||||||
private var tabSelection: String = "Home"
|
@State private var librariesShowRecentlyAdded: [String] = []
|
||||||
@State
|
@State private var libraryPrefillID: String = ""
|
||||||
private var libraries: [String] = []
|
@State private var showSettingsPopover: Bool = false
|
||||||
@State
|
@State private var viewDidLoad: Bool = false
|
||||||
private var library_names: [String: String] = [:]
|
|
||||||
@State
|
|
||||||
private var librariesShowRecentlyAdded: [String] = []
|
|
||||||
@State
|
|
||||||
private var libraryPrefillID: String = ""
|
|
||||||
@State
|
|
||||||
private var showSettingsPopover: Bool = false
|
|
||||||
@State
|
|
||||||
private var viewDidLoad: Bool = false
|
|
||||||
|
|
||||||
func startup() {
|
func startup() {
|
||||||
|
if(viewDidLoad == true) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
viewDidLoad = true
|
||||||
|
|
||||||
let size = UIScreen.main.bounds.size
|
let size = UIScreen.main.bounds.size
|
||||||
if size.width < size.height {
|
if size.width < size.height {
|
||||||
orientationInfo.orientation = .portrait
|
orientationInfo.orientation = .portrait
|
||||||
|
@ -60,12 +49,6 @@ struct ContentView: View {
|
||||||
orientationInfo.orientation = .landscape
|
orientationInfo.orientation = .landscape
|
||||||
}
|
}
|
||||||
|
|
||||||
if viewDidLoad {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
viewDidLoad = true
|
|
||||||
|
|
||||||
ImageCache.shared.costLimit = 125 * 1024 * 1024 // 125MB memory
|
ImageCache.shared.costLimit = 125 * 1024 * 1024 // 125MB memory
|
||||||
DataLoader.sharedUrlCache.diskCapacity = 1000 * 1024 * 1024 // 1000MB disk
|
DataLoader.sharedUrlCache.diskCapacity = 1000 * 1024 * 1024 // 1000MB disk
|
||||||
|
|
||||||
|
@ -91,19 +74,18 @@ struct ContentView: View {
|
||||||
var header = "MediaBrowser "
|
var header = "MediaBrowser "
|
||||||
header.append("Client=\"SwiftFin\", ")
|
header.append("Client=\"SwiftFin\", ")
|
||||||
header.append("Device=\"\(deviceName)\", ")
|
header.append("Device=\"\(deviceName)\", ")
|
||||||
header.append("DeviceId=\"\(globalData.user?.device_uuid ?? "")\", ")
|
header.append("DeviceId=\"\(globalData.user.device_uuid ?? "")\", ")
|
||||||
header.append("Version=\"\(appVersion ?? "0.0.1")\", ")
|
header.append("Version=\"\(appVersion ?? "0.0.1")\", ")
|
||||||
header.append("Token=\"\(globalData.authToken)\"")
|
header.append("Token=\"\(globalData.authToken)\"")
|
||||||
|
|
||||||
globalData.authHeader = header
|
globalData.authHeader = header
|
||||||
JellyfinAPI.basePath = globalData.server?.baseURI ?? ""
|
JellyfinAPI.basePath = globalData.server.baseURI ?? ""
|
||||||
JellyfinAPI.customHeaders = ["X-Emby-Authorization": globalData.authHeader]
|
JellyfinAPI.customHeaders = ["X-Emby-Authorization": globalData.authHeader]
|
||||||
|
|
||||||
UserAPI.getCurrentUser()
|
UserAPI.getCurrentUser()
|
||||||
.sink(receiveCompletion: { completion in
|
.sink(receiveCompletion: { completion in
|
||||||
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
|
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
|
||||||
}, receiveValue: { response in
|
}, receiveValue: { response in
|
||||||
//Get all libraries
|
|
||||||
libraries = response.configuration?.orderedViews ?? []
|
libraries = response.configuration?.orderedViews ?? []
|
||||||
librariesShowRecentlyAdded = libraries.filter { element in
|
librariesShowRecentlyAdded = libraries.filter { element in
|
||||||
return !(response.configuration?.latestItemsExcludes?.contains(element))!
|
return !(response.configuration?.latestItemsExcludes?.contains(element))!
|
||||||
|
@ -111,11 +93,10 @@ struct ContentView: View {
|
||||||
})
|
})
|
||||||
.store(in: &globalData.pendingAPIRequests)
|
.store(in: &globalData.pendingAPIRequests)
|
||||||
|
|
||||||
UserViewsAPI.getUserViews(userId: globalData.user?.user_id ?? "")
|
UserViewsAPI.getUserViews(userId: globalData.user.user_id ?? "")
|
||||||
.sink(receiveCompletion: { completion in
|
.sink(receiveCompletion: { completion in
|
||||||
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
|
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
|
||||||
}, receiveValue: { response in
|
}, receiveValue: { response in
|
||||||
//Get all libraries
|
|
||||||
response.items?.forEach({ item in
|
response.items?.forEach({ item in
|
||||||
library_names[item.id ?? ""] = item.name
|
library_names[item.id ?? ""] = item.name
|
||||||
})
|
})
|
||||||
|
@ -144,7 +125,7 @@ struct ContentView: View {
|
||||||
} else if (globalData.expiredCredentials == true) {
|
} else if (globalData.expiredCredentials == true) {
|
||||||
NavigationView {
|
NavigationView {
|
||||||
ConnectToServerView(skip_server: true, skip_server_prefill: globalData.server,
|
ConnectToServerView(skip_server: true, skip_server_prefill: globalData.server,
|
||||||
reauth_deviceId: globalData.user?.device_uuid ?? "", isActive: $globalData.expiredCredentials)
|
reauth_deviceId: globalData.user.device_uuid ?? "", isActive: $globalData.expiredCredentials)
|
||||||
}
|
}
|
||||||
.navigationViewStyle(StackNavigationViewStyle())
|
.navigationViewStyle(StackNavigationViewStyle())
|
||||||
.environmentObject(globalData)
|
.environmentObject(globalData)
|
||||||
|
@ -155,9 +136,9 @@ struct ContentView: View {
|
||||||
NavigationView {
|
NavigationView {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
Spacer().frame(height: orientationInfo.orientation == .portrait ? 0 : 15)
|
Spacer().frame(height: orientationInfo.orientation == .portrait ? 0 : 16)
|
||||||
ContinueWatchingView()
|
ContinueWatchingView()
|
||||||
NextUpView().padding(EdgeInsets(top: 4, leading: 0, bottom: 0, trailing: 0))
|
NextUpView()
|
||||||
ForEach(librariesShowRecentlyAdded, id: \.self) { library_id in
|
ForEach(librariesShowRecentlyAdded, id: \.self) { library_id in
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
HStack {
|
HStack {
|
||||||
|
@ -171,7 +152,7 @@ struct ContentView: View {
|
||||||
Text("See All").font(.subheadline).fontWeight(.bold)
|
Text("See All").font(.subheadline).fontWeight(.bold)
|
||||||
}
|
}
|
||||||
}.padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16))
|
}.padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16))
|
||||||
LatestMediaView(library: library_id)
|
LatestMediaView(usingLibraryID: library_id)
|
||||||
}.padding(EdgeInsets(top: 4, leading: 0, bottom: 0, trailing: 0))
|
}.padding(EdgeInsets(top: 4, leading: 0, bottom: 0, trailing: 0))
|
||||||
}
|
}
|
||||||
Spacer().frame(height: UIDevice.current.userInterfaceIdiom == .phone ? 20 : 30)
|
Spacer().frame(height: UIDevice.current.userInterfaceIdiom == .phone ? 20 : 30)
|
||||||
|
@ -211,10 +192,10 @@ struct ContentView: View {
|
||||||
.environmentObject(globalData)
|
.environmentObject(globalData)
|
||||||
.onAppear(perform: startup)
|
.onAppear(perform: startup)
|
||||||
.alert(isPresented: $globalData.networkError) {
|
.alert(isPresented: $globalData.networkError) {
|
||||||
Alert(title: Text("Network Error"), message: Text("Couldn't connect to Jellyfin"), dismissButton: .default(Text("Ok")))
|
Alert(title: Text("Network Error"), message: Text("An error occured while performing a network request"), dismissButton: .default(Text("Ok")))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Text("Signing in...")
|
Text("Please wait...")
|
||||||
.onAppear(perform: {
|
.onAppear(perform: {
|
||||||
DispatchQueue.main.async { [self] in
|
DispatchQueue.main.async { [self] in
|
||||||
_viewDidLoad.wrappedValue = false
|
_viewDidLoad.wrappedValue = false
|
||||||
|
|
|
@ -6,27 +6,24 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import SwiftyRequest
|
|
||||||
import SwiftyJSON
|
|
||||||
import NukeUI
|
import NukeUI
|
||||||
|
import JellyfinAPI
|
||||||
|
|
||||||
struct CustomShape: Shape {
|
struct ProgressBar: Shape {
|
||||||
let radius: CGFloat
|
|
||||||
|
|
||||||
func path(in rect: CGRect) -> Path {
|
func path(in rect: CGRect) -> Path {
|
||||||
var path = Path()
|
var path = Path()
|
||||||
|
|
||||||
let tl = CGPoint(x: rect.minX, y: rect.minY)
|
let tl = CGPoint(x: rect.minX, y: rect.minY)
|
||||||
let tr = CGPoint(x: rect.maxX, y: rect.minY)
|
let tr = CGPoint(x: rect.maxX, y: rect.minY)
|
||||||
let br = CGPoint(x: rect.maxX, y: rect.maxY)
|
let br = CGPoint(x: rect.maxX, y: rect.maxY)
|
||||||
let bls = CGPoint(x: rect.minX + radius, y: rect.maxY)
|
let bls = CGPoint(x: rect.minX + 10, y: rect.maxY)
|
||||||
let blc = CGPoint(x: rect.minX + radius, y: rect.maxY - radius)
|
let blc = CGPoint(x: rect.minX + 10, y: rect.maxY - 10)
|
||||||
|
|
||||||
path.move(to: tl)
|
path.move(to: tl)
|
||||||
path.addLine(to: tr)
|
path.addLine(to: tr)
|
||||||
path.addLine(to: br)
|
path.addLine(to: br)
|
||||||
path.addLine(to: bls)
|
path.addLine(to: bls)
|
||||||
path.addRelativeArc(center: blc, radius: radius,
|
path.addRelativeArc(center: blc, radius: 10,
|
||||||
startAngle: Angle.degrees(90), delta: Angle.degrees(90))
|
startAngle: Angle.degrees(90), delta: Angle.degrees(90))
|
||||||
|
|
||||||
return path
|
return path
|
||||||
|
@ -35,147 +32,77 @@ struct CustomShape: Shape {
|
||||||
|
|
||||||
|
|
||||||
struct ContinueWatchingView: View {
|
struct ContinueWatchingView: View {
|
||||||
@Environment(\.managedObjectContext) private var viewContext
|
|
||||||
@EnvironmentObject var globalData: GlobalData
|
@EnvironmentObject var globalData: GlobalData
|
||||||
@EnvironmentObject var orientationInfo: OrientationInfo
|
|
||||||
|
|
||||||
@State var resumeItems: [ResumeItem] = []
|
@State private var items: [BaseItemDto] = []
|
||||||
@State private var viewDidLoad: Int = 0;
|
|
||||||
@State private var isLoading: Bool = true;
|
|
||||||
|
|
||||||
func onAppear() {
|
func onAppear() {
|
||||||
if(globalData.server?.baseURI == "") {
|
ItemsAPI.getResumeItems(userId: globalData.user.user_id ?? "", limit: 12, fields: [.primaryImageAspectRatio], mediaTypes: ["Video"], imageTypeLimit: 1, enableImageTypes: [.primary,.backdrop,.thumb])
|
||||||
return
|
.sink(receiveCompletion: { completion in
|
||||||
}
|
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
|
||||||
if(viewDidLoad == 1) {
|
}, receiveValue: { response in
|
||||||
return
|
items = response.items ?? []
|
||||||
}
|
})
|
||||||
_viewDidLoad.wrappedValue = 1;
|
.store(in: &globalData.pendingAPIRequests)
|
||||||
let request = RestRequest(method: .get, url: (globalData.server?.baseURI ?? "") + "/Users/\(globalData.user?.user_id ?? "")/Items/Resume?Limit=12&Recursive=true&Fields=PrimaryImageAspectRatio%2CBasicSyncInfo&ImageTypeLimit=1&EnableImageTypes=Primary%2CBackdrop%2CThumb&MediaTypes=Video")
|
|
||||||
request.headerParameters["X-Emby-Authorization"] = globalData.authHeader
|
|
||||||
request.contentType = "application/json"
|
|
||||||
request.acceptType = "application/json"
|
|
||||||
|
|
||||||
request.responseData() { (result: Result<RestResponse<Data>, RestError>) in
|
|
||||||
switch result {
|
|
||||||
case .success(let response):
|
|
||||||
let body = response.body
|
|
||||||
do {
|
|
||||||
let json = try JSON(data: body)
|
|
||||||
for (_,item):(String, JSON) in json["Items"] {
|
|
||||||
// Do something you want
|
|
||||||
var itemObj = ResumeItem()
|
|
||||||
if(item["PrimaryImageAspectRatio"].double ?? 0.0 < 1.0) {
|
|
||||||
//portrait; use backdrop instead
|
|
||||||
itemObj.Image = item["BackdropImageTags"][0].string ?? ""
|
|
||||||
itemObj.ImageType = "Backdrop"
|
|
||||||
|
|
||||||
if(itemObj.Image == "") {
|
|
||||||
itemObj.Image = item["ParentBackdropImageTags"][0].string ?? ""
|
|
||||||
}
|
|
||||||
|
|
||||||
itemObj.BlurHash = item["ImageBlurHashes"]["Backdrop"][itemObj.Image].string ?? ""
|
|
||||||
} else {
|
|
||||||
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.ItemProgress = item["UserData"]["PlayedPercentage"].double ?? 0.00
|
|
||||||
_resumeItems.wrappedValue.append(itemObj)
|
|
||||||
}
|
|
||||||
_isLoading.wrappedValue = false;
|
|
||||||
} catch {
|
|
||||||
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case .failure(let error):
|
|
||||||
_viewDidLoad.wrappedValue = 0;
|
|
||||||
debugPrint(error)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView(.horizontal, showsIndicators: false) {
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
if(_resumeItems.wrappedValue.count > 0) {
|
if(items.count > 0) {
|
||||||
LazyHStack() {
|
LazyHStack() {
|
||||||
Spacer().frame(width:12)
|
Spacer().frame(width:14)
|
||||||
ForEach(resumeItems, id: \.Id) { item in
|
ForEach(items, id: \.id) { item in
|
||||||
NavigationLink(destination: ItemView(item: item)) {
|
NavigationLink(destination: EmptyView()) {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
Spacer().frame(height: 10)
|
Spacer().frame(height: 10)
|
||||||
if(item.Type == "Episode") {
|
LazyImage(source: item.getBackdropImage(baseURL: globalData.server.baseURI ?? "", maxWidth: 320))
|
||||||
LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?maxWidth=550&quality=80&tag=\(item.Image)"))
|
.placeholderAndFailure {
|
||||||
.placeholderAndFailure {
|
Image(uiImage: UIImage(blurHash: item.getBackdropImageBlurHash(), size: CGSize(width: 48, height: 32))!)
|
||||||
Image(uiImage: UIImage(blurHash: (item.BlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item.BlurHash), size: CGSize(width: 48, height: 32))!)
|
.resizable()
|
||||||
.resizable()
|
.aspectRatio(contentMode: .fill)
|
||||||
.frame(width: 320, height: 180)
|
.frame(width: 320, height: 180)
|
||||||
.cornerRadius(10)
|
.cornerRadius(10)
|
||||||
}
|
}
|
||||||
.frame(width: 320, height: 180)
|
.aspectRatio(contentMode: .fill)
|
||||||
.cornerRadius(10)
|
.frame(width: 320, height: 180)
|
||||||
.overlay(
|
.cornerRadius(10)
|
||||||
ZStack {
|
.overlay(
|
||||||
Text("S\(String(item.ParentIndexNumber ?? 0)):E\(String(item.IndexNumber ?? 0)) - \(item.Name)")
|
Group {
|
||||||
|
if(item.type == "Episode") {
|
||||||
|
Text("\(item.name!)")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.padding(6)
|
.padding(6)
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
}.background(Color.black)
|
}
|
||||||
.opacity(0.8)
|
}.background(Color.black)
|
||||||
.cornerRadius(10.0)
|
.opacity(0.8)
|
||||||
.padding(6), alignment: .topTrailing
|
.cornerRadius(10.0)
|
||||||
)
|
.padding(6), alignment: .topTrailing
|
||||||
.overlay(
|
)
|
||||||
Rectangle()
|
.overlay(
|
||||||
.fill(Color(red: 172/255, green: 92/255, blue: 195/255))
|
Rectangle()
|
||||||
.mask(CustomShape(radius: 10))
|
.fill(Color(red: 172/255, green: 92/255, blue: 195/255))
|
||||||
.frame(width: CGFloat((item.ItemProgress/100)*320), height: 7)
|
.mask(ProgressBar())
|
||||||
.padding(0), alignment: .bottomLeading
|
.frame(width: CGFloat(item.userData!.playedPercentage!*3.2), height: 7)
|
||||||
)
|
|
||||||
} else {
|
|
||||||
LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?maxWidth=550&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: 48, height: 32))!)
|
|
||||||
.resizable()
|
|
||||||
.frame(width: 320, height: 180)
|
|
||||||
.cornerRadius(10)
|
|
||||||
}
|
|
||||||
.frame(width: 320, height: 180)
|
|
||||||
.cornerRadius(10)
|
|
||||||
.overlay(
|
|
||||||
Rectangle()
|
|
||||||
.fill(Color(red: 172/255, green: 92/255, blue: 195/255))
|
|
||||||
.mask(CustomShape(radius: 10))
|
|
||||||
.frame(width: CGFloat((item.ItemProgress/100)*320), height: 7)
|
|
||||||
.padding(0), alignment: .bottomLeading
|
.padding(0), alignment: .bottomLeading
|
||||||
)
|
)
|
||||||
}
|
Text(item.seriesName ?? item.name ?? "")
|
||||||
Text("\(item.Type == "Episode" ? item.SeriesName ?? "" : item.Name)")
|
|
||||||
.font(.callout)
|
.font(.callout)
|
||||||
.fontWeight(.semibold)
|
.fontWeight(.semibold)
|
||||||
.foregroundColor(.primary)
|
.foregroundColor(.primary)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
.frame(width: 320, alignment: .leading)
|
.frame(width: 320, alignment: .leading)
|
||||||
Spacer().frame(height: 5)
|
Spacer().frame(height: 5)
|
||||||
}.padding(.trailing, 5)
|
}
|
||||||
}
|
}
|
||||||
|
Spacer().frame(width: 16)
|
||||||
}
|
}
|
||||||
Spacer().frame(width: 2)
|
Spacer().frame(width: 2)
|
||||||
}.frame(height: 215)
|
}.frame(height: 215)
|
||||||
|
.padding(.bottom, 10)
|
||||||
} else {
|
} else {
|
||||||
EmptyView()
|
EmptyView()
|
||||||
}
|
}
|
||||||
}.onAppear(perform: onAppear)
|
}.onAppear(perform: onAppear)
|
||||||
.padding(.bottom, 10)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
//lol can someone buy me a coffee this took forever :|
|
//lol can someone buy me a coffee this took forever :|
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import SwiftyJSON
|
import JellyfinAPI
|
||||||
|
|
||||||
enum CPUModel {
|
enum CPUModel {
|
||||||
case A4
|
case A4
|
||||||
|
@ -33,65 +33,6 @@ enum CPUModel {
|
||||||
case A99
|
case A99
|
||||||
}
|
}
|
||||||
|
|
||||||
struct _AVDirectProfile: Codable {
|
|
||||||
var Container: String;
|
|
||||||
var `Type`: String;
|
|
||||||
var AudioCodec: String = "";
|
|
||||||
var VideoCodec: String = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
struct _AVTranscodingProfile: Codable {
|
|
||||||
var Container: String;
|
|
||||||
var `Type`: String;
|
|
||||||
var AudioCodec: String = "";
|
|
||||||
var VideoCodec: String = "";
|
|
||||||
var Context: String = "";
|
|
||||||
var `Protocol`: String = "hls";
|
|
||||||
var MaxAudioChannels: String = "6";
|
|
||||||
var MinSegments: String = "2";
|
|
||||||
var BreakOnNonKeyFrames: Bool = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct _AVCodecCondition: Codable {
|
|
||||||
var Condition: String;
|
|
||||||
var Property: String;
|
|
||||||
var Value: String;
|
|
||||||
var IsRequired: Bool;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct _AVCodecProfile: Codable {
|
|
||||||
var `Type`: String;
|
|
||||||
var Codec: String = "";
|
|
||||||
var Conditions: [_AVCodecCondition] = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
struct _AVSubtitleProfile: Codable {
|
|
||||||
var Format: String;
|
|
||||||
var Method: String;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct _AVResponseProfile: Codable {
|
|
||||||
var `Type`: String;
|
|
||||||
var Container: String;
|
|
||||||
var MimeType: String;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct DeviceProfile: Codable {
|
|
||||||
var MaxStreamingBitrate: Int;
|
|
||||||
var MaxStaticBitrate: Int;
|
|
||||||
var MusicStreamingTranscodingBitrate: Int;
|
|
||||||
var DirectPlayProfiles: [_AVDirectProfile] = [];
|
|
||||||
var TranscodingProfiles: [_AVTranscodingProfile] = [];
|
|
||||||
var ContainerProfiles: [_AVDirectProfile] = [];
|
|
||||||
var CodecProfiles: [_AVCodecProfile] = [];
|
|
||||||
var SubtitleProfiles: [_AVSubtitleProfile] = [];
|
|
||||||
var ResponseProfiles: [_AVResponseProfile] = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
struct DeviceProfileRoot: Codable {
|
|
||||||
var DeviceProfile: DeviceProfile;
|
|
||||||
}
|
|
||||||
|
|
||||||
class DeviceProfileBuilder {
|
class DeviceProfileBuilder {
|
||||||
public var bitrate: Int = 0;
|
public var bitrate: Int = 0;
|
||||||
|
|
||||||
|
@ -99,92 +40,91 @@ class DeviceProfileBuilder {
|
||||||
self.bitrate = bitrate
|
self.bitrate = bitrate
|
||||||
}
|
}
|
||||||
|
|
||||||
public func buildProfile() -> DeviceProfileRoot {
|
public func buildProfile() -> DeviceProfile {
|
||||||
print(CPUinfo())
|
let maxStreamingBitrate = bitrate;
|
||||||
let MaxStreamingBitrate = bitrate;
|
let maxStaticBitrate = bitrate;
|
||||||
let MaxStaticBitrate = bitrate;
|
let musicStreamingTranscodingBitrate = 384000;
|
||||||
let MusicStreamingTranscodingBitrate = 384000;
|
|
||||||
|
|
||||||
//Build direct play profiles
|
//Build direct play profiles
|
||||||
var DirectPlayProfiles: [_AVDirectProfile] = [];
|
var directPlayProfiles: [DirectPlayProfile] = [];
|
||||||
DirectPlayProfiles = [_AVDirectProfile(Container: "mov,mp4,mkv", Type: "Video", AudioCodec: "aac,mp3,wav", VideoCodec: "h264")]
|
directPlayProfiles = [DirectPlayProfile(container: "mov,mp4,mkv", audioCodec: "aac,mp3,wav", videoCodec: "h264", type: .video)]
|
||||||
|
|
||||||
//Device supports Dolby Digital (AC3, EAC3)
|
//Device supports Dolby Digital (AC3, EAC3)
|
||||||
if(supportsFeature(minimumSupported: .A8X)) {
|
if(supportsFeature(minimumSupported: .A8X)) {
|
||||||
if(supportsFeature(minimumSupported: .A10)) {
|
if(supportsFeature(minimumSupported: .A10)) {
|
||||||
DirectPlayProfiles = [_AVDirectProfile(Container: "mov,mp4,mkv", Type: "Video", AudioCodec: "aac,mp3,wav,ac3,eac3,flac,opus", VideoCodec: "hevc,h264,hev1")] //HEVC/H.264 with Dolby Digital
|
directPlayProfiles = [DirectPlayProfile(container: "mov,mp4,mkv", audioCodec: "aac,mp3,wav,ac3,eac3,flac,opus", videoCodec: "hevc,h264,hev1", type: .video)] //HEVC/H.264 with Dolby Digital
|
||||||
} else {
|
} else {
|
||||||
DirectPlayProfiles = [_AVDirectProfile(Container: "mov,mp4,mkv", Type: "Video", AudioCodec: "ac3,eac3,aac,mp3,wav,opus", VideoCodec: "h264")] //H.264 with Dolby Digital
|
directPlayProfiles = [DirectPlayProfile(container: "mov,mp4,mkv", audioCodec: "ac3,eac3,aac,mp3,wav,opus", videoCodec: "h264", type: .video)] //H.264 with Dolby Digital
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//Device supports Dolby Vision?
|
//Device supports Dolby Vision?
|
||||||
if(supportsFeature(minimumSupported: .A10X)) {
|
if(supportsFeature(minimumSupported: .A10X)) {
|
||||||
DirectPlayProfiles = [_AVDirectProfile(Container: "mov,mp4,mkv", Type: "Video", AudioCodec: "aac,mp3,wav,ac3,eac3,flac,opus", VideoCodec: "dvhe,dvh1,dva1,dvav,h264,hevc,hev1")] //H.264/HEVC with Dolby Digital - No Atmos - Vision
|
directPlayProfiles = [DirectPlayProfile(container: "mov,mp4,mkv", audioCodec: "aac,mp3,wav,ac3,eac3,flac,opus", videoCodec: "dvhe,dvh1,dva1,dvav,h264,hevc,hev1", type: .video)] //H.264/HEVC with Dolby Digital - No Atmos - Vision
|
||||||
}
|
}
|
||||||
|
|
||||||
//Device supports Dolby Atmos?
|
//Device supports Dolby Atmos?
|
||||||
if(supportsFeature(minimumSupported: .A12)) {
|
if(supportsFeature(minimumSupported: .A12)) {
|
||||||
DirectPlayProfiles = [_AVDirectProfile(Container: "mov,mp4,mkv", Type: "Video", AudioCodec: "aac,mp3,wav,ac3,eac3,flac,truehd,dts,dca,opus", VideoCodec: "h264,hevc,dvhe,dvh1,dva1,dvav,h264,hevc,hev1")] //H.264/HEVC with Dolby Digital & Atmos - Vision
|
directPlayProfiles = [DirectPlayProfile(container: "mov,mp4,mkv", audioCodec: "aac,mp3,wav,ac3,eac3,flac,truehd,dts,dca,opus", videoCodec: "h264,hevc,dvhe,dvh1,dva1,dvav,h264,hevc,hev1", type: .video)] //H.264/HEVC with Dolby Digital & Atmos - Vision
|
||||||
}
|
}
|
||||||
|
|
||||||
//Build transcoding profiles
|
//Build transcoding profiles
|
||||||
var TranscodingProfiles: [_AVTranscodingProfile] = [];
|
var transcodingProfiles: [TranscodingProfile] = [];
|
||||||
TranscodingProfiles = [_AVTranscodingProfile(Container: "ts", Type: "Video", AudioCodec: "aac,mp3,wav", VideoCodec: "h264", Context: "Streaming", Protocol: "hls", MaxAudioChannels: "2", MinSegments: "2", BreakOnNonKeyFrames: true)]
|
transcodingProfiles = [TranscodingProfile(container: "ts", type: .video, videoCodec: "h264", audioCodec: "aac,mp3,wav")]
|
||||||
|
|
||||||
//Device supports Dolby Digital (AC3, EAC3)
|
//Device supports Dolby Digital (AC3, EAC3)
|
||||||
if(supportsFeature(minimumSupported: .A8X)) {
|
if(supportsFeature(minimumSupported: .A8X)) {
|
||||||
if(supportsFeature(minimumSupported: .A10)) {
|
if(supportsFeature(minimumSupported: .A10)) {
|
||||||
TranscodingProfiles = [_AVTranscodingProfile(Container: "ts", Type: "Video", AudioCodec: "aac,mp3,wav,eac3,ac3,flac,opus", VideoCodec: "h264,hevc,hev1", Context: "Streaming", Protocol: "hls", MaxAudioChannels: "6", MinSegments: "2", BreakOnNonKeyFrames: true)]
|
transcodingProfiles = [TranscodingProfile(container: "ts", type: .video, videoCodec: "aac,mp3,wav,eac3,ac3,flac,opus", audioCodec: "h264,hevc,hev1", _protocol: "hls", context: .streaming, maxAudioChannels: "6", minSegments: 2, breakOnNonKeyFrames: true)]
|
||||||
} else {
|
} else {
|
||||||
TranscodingProfiles = [_AVTranscodingProfile(Container: "ts", Type: "Video", AudioCodec: "aac,mp3,wav,eac3,ac3,opus", VideoCodec: "h264", Context: "Streaming", Protocol: "hls", MaxAudioChannels: "2", MinSegments: "2", BreakOnNonKeyFrames: true)]
|
transcodingProfiles = [TranscodingProfile(container: "ts", type: .video, videoCodec: "h264", audioCodec: "aac,mp3,wav,eac3,ac3,opus", _protocol: "hls", context: .streaming, maxAudioChannels: "6", minSegments: 2, breakOnNonKeyFrames: true)]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//Device supports Dolby Vision?
|
//Device supports Dolby Vision?
|
||||||
if(supportsFeature(minimumSupported: .A10X)) {
|
if(supportsFeature(minimumSupported: .A10X)) {
|
||||||
TranscodingProfiles = [_AVTranscodingProfile(Container: "ts", Type: "Video", AudioCodec: "aac,mp3,wav,ac3,eac3,flac,opus", VideoCodec: "dva1,dvav,dvhe,dvh1,hevc,h264,hev1", Context: "Streaming", Protocol: "hls", MaxAudioChannels: "6", MinSegments: "2", BreakOnNonKeyFrames: true)]
|
transcodingProfiles = [TranscodingProfile(container: "ts", type: .video, videoCodec: "dva1,dvav,dvhe,dvh1,hevc,h264,hev1", audioCodec: "aac,mp3,wav,ac3,eac3,flac,opus", _protocol: "hls", context: .streaming, maxAudioChannels: "6", minSegments: 2, breakOnNonKeyFrames: true)]
|
||||||
}
|
}
|
||||||
|
|
||||||
//Device supports Dolby Atmos?
|
//Device supports Dolby Atmos?
|
||||||
if(supportsFeature(minimumSupported: .A12)) {
|
if(supportsFeature(minimumSupported: .A12)) {
|
||||||
TranscodingProfiles = [_AVTranscodingProfile(Container: "ts", Type: "Video", AudioCodec: "aac,mp3,wav,ac3,eac3,flac,dts,truehd,dca,opus", VideoCodec: "dva1,dvav,dvhe,dvh1,hevc,h264,hev1", Context: "Streaming", Protocol: "hls", MaxAudioChannels: "9", MinSegments: "2", BreakOnNonKeyFrames: true)]
|
transcodingProfiles = [TranscodingProfile(container: "ts", type: .video, videoCodec: "dva1,dvav,dvhe,dvh1,hevc,h264,hev1", audioCodec: "aac,mp3,wav,ac3,eac3,flac,dts,truehd,dca,opus", _protocol: "hls", context: .streaming, maxAudioChannels: "6", minSegments: 2, breakOnNonKeyFrames: true)]
|
||||||
}
|
}
|
||||||
|
|
||||||
var CodecProfiles: [_AVCodecProfile] = []
|
var codecProfiles: [CodecProfile] = []
|
||||||
|
|
||||||
let h264CodecConditions: [_AVCodecCondition] = [
|
let h264CodecConditions: [ProfileCondition] = [
|
||||||
_AVCodecCondition(Condition: "NotEquals", Property: "IsAnamorphic", Value: "true", IsRequired: false),
|
ProfileCondition(condition: .notEquals, property: .isAnamorphic, value: "true", isRequired: false),
|
||||||
_AVCodecCondition(Condition: "EqualsAny", Property: "VideoProfile", Value: "high|main|baseline|constrained baseline", IsRequired: false),
|
ProfileCondition(condition: .equalsAny, property: .videoProfile, value: "high|main|baseline|constrained baseline", isRequired: false),
|
||||||
_AVCodecCondition(Condition: "LessThanEqual", Property: "VideoLevel", Value: "60", IsRequired: false),
|
ProfileCondition(condition: .lessThanEqual, property: .videoLevel, value: "60", isRequired: false),
|
||||||
_AVCodecCondition(Condition: "NotEquals", Property: "IsInterlaced", Value: "true", IsRequired: false)]
|
ProfileCondition(condition: .notEquals, property: .isInterlaced, value: "true", isRequired: false)]
|
||||||
let hevcCodecConditions: [_AVCodecCondition] = [
|
let hevcCodecConditions: [ProfileCondition] = [
|
||||||
_AVCodecCondition(Condition: "NotEquals", Property: "IsAnamorphic", Value: "true", IsRequired: false),
|
ProfileCondition(condition: .notEquals, property: .isAnamorphic, value: "true", isRequired: false),
|
||||||
_AVCodecCondition(Condition: "EqualsAny", Property: "VideoProfile", Value: "main|main 10", IsRequired: false),
|
ProfileCondition(condition: .equalsAny, property: .videoProfile, value: "main|main 10", isRequired: false),
|
||||||
_AVCodecCondition(Condition: "LessThanEqual", Property: "VideoLevel", Value: "160", IsRequired: false),
|
ProfileCondition(condition: .lessThanEqual, property: .videoLevel, value: "160", isRequired: false),
|
||||||
_AVCodecCondition(Condition: "NotEquals", Property: "IsInterlaced", Value: "true", IsRequired: false)]
|
ProfileCondition(condition: .notEquals, property: .isInterlaced, value: "true", isRequired: false)]
|
||||||
|
|
||||||
CodecProfiles.append(_AVCodecProfile(Type: "Video", Codec: "h264", Conditions: h264CodecConditions))
|
codecProfiles.append(CodecProfile(type: .video, applyConditions: h264CodecConditions, codec: "h264"))
|
||||||
|
|
||||||
if(supportsFeature(minimumSupported: .A10)) {
|
if(supportsFeature(minimumSupported: .A10)) {
|
||||||
CodecProfiles.append(_AVCodecProfile(Type: "Video", Codec: "hevc", Conditions: hevcCodecConditions))
|
codecProfiles.append(CodecProfile(type: .video, applyConditions: hevcCodecConditions,codec: "hevc"))
|
||||||
}
|
}
|
||||||
|
|
||||||
var SubtitleProfiles: [_AVSubtitleProfile] = []
|
var subtitleProfiles: [SubtitleProfile] = []
|
||||||
SubtitleProfiles.append(_AVSubtitleProfile(Format: "vtt", Method: "External"))
|
subtitleProfiles.append(SubtitleProfile(format: "vtt", method: .external))
|
||||||
SubtitleProfiles.append(_AVSubtitleProfile(Format: "ass", Method: "External"))
|
subtitleProfiles.append(SubtitleProfile(format: "ass", method: .external))
|
||||||
SubtitleProfiles.append(_AVSubtitleProfile(Format: "ssa", Method: "External"))
|
subtitleProfiles.append(SubtitleProfile(format: "ssa", method: .external))
|
||||||
SubtitleProfiles.append(_AVSubtitleProfile(Format: "pgssub", Method: "Embed"))
|
subtitleProfiles.append(SubtitleProfile(format: "pgssub", method: .embed))
|
||||||
SubtitleProfiles.append(_AVSubtitleProfile(Format: "sub", Method: "Embed"))
|
subtitleProfiles.append(SubtitleProfile(format: "sub", method: .embed))
|
||||||
SubtitleProfiles.append(_AVSubtitleProfile(Format: "rip", Method: "Embed"))
|
subtitleProfiles.append(SubtitleProfile(format: "rip", method: .embed))
|
||||||
SubtitleProfiles.append(_AVSubtitleProfile(Format: "srt", Method: "Embed"))
|
subtitleProfiles.append(SubtitleProfile(format: "srt", method: .embed))
|
||||||
SubtitleProfiles.append(_AVSubtitleProfile(Format: "pgs", Method: "Embed"))
|
subtitleProfiles.append(SubtitleProfile(format: "pgs", method: .embed))
|
||||||
|
|
||||||
let ResponseProfiles: [_AVResponseProfile] = [_AVResponseProfile(Type: "Video", Container: "m4v", MimeType: "video/mp4")]
|
let responseProfiles: [ResponseProfile] = [ResponseProfile(container: "m4v", type: .video, mimeType: "video/mp4")]
|
||||||
|
|
||||||
let DP = DeviceProfile(MaxStreamingBitrate: MaxStreamingBitrate, MaxStaticBitrate: MaxStaticBitrate, MusicStreamingTranscodingBitrate: MusicStreamingTranscodingBitrate, DirectPlayProfiles: DirectPlayProfiles, TranscodingProfiles: TranscodingProfiles, CodecProfiles: CodecProfiles, SubtitleProfiles: SubtitleProfiles, ResponseProfiles: ResponseProfiles)
|
let profile = DeviceProfile(maxStreamingBitrate: maxStreamingBitrate, maxStaticBitrate: maxStaticBitrate, musicStreamingTranscodingBitrate: musicStreamingTranscodingBitrate, directPlayProfiles: directPlayProfiles, transcodingProfiles: transcodingProfiles, containerProfiles: [], codecProfiles: codecProfiles, responseProfiles: responseProfiles, subtitleProfiles: subtitleProfiles)
|
||||||
|
|
||||||
return DeviceProfileRoot(DeviceProfile: DP)
|
return profile
|
||||||
}
|
}
|
||||||
|
|
||||||
private func supportsFeature(minimumSupported: CPUModel) -> Bool {
|
private func supportsFeature(minimumSupported: CPUModel) -> Bool {
|
||||||
|
|
|
@ -37,8 +37,8 @@ struct EpisodeItemView: View {
|
||||||
formatter.timeZone = TimeZone(secondsFromGMT: 0)
|
formatter.timeZone = TimeZone(secondsFromGMT: 0)
|
||||||
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ"
|
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ"
|
||||||
let request = RestRequest(method: .post,
|
let request = RestRequest(method: .post,
|
||||||
url: (globalData.server?.baseURI ?? "") +
|
url: (globalData.server.baseURI ?? "") +
|
||||||
"/Users/\(globalData.user?.user_id ?? "")/PlayedItems/\(fullItem.Id)?DatePlayed=\(formatter.string(from: date).replacingOccurrences(of: ":", with: "%3A"))")
|
"/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.headerParameters["X-Emby-Authorization"] = globalData.authHeader
|
||||||
request.contentType = "application/json"
|
request.contentType = "application/json"
|
||||||
request.acceptType = "application/json"
|
request.acceptType = "application/json"
|
||||||
|
@ -47,8 +47,8 @@ struct EpisodeItemView: View {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let request = RestRequest(method: .delete,
|
let request = RestRequest(method: .delete,
|
||||||
url: (globalData.server?.baseURI ?? "") +
|
url: (globalData.server.baseURI ?? "") +
|
||||||
"/Users/\(globalData.user?.user_id ?? "")/PlayedItems/\(fullItem.Id)")
|
"/Users/\(globalData.user.user_id ?? "")/PlayedItems/\(fullItem.Id)")
|
||||||
request.headerParameters["X-Emby-Authorization"] = globalData.authHeader
|
request.headerParameters["X-Emby-Authorization"] = globalData.authHeader
|
||||||
request.contentType = "application/json"
|
request.contentType = "application/json"
|
||||||
request.acceptType = "application/json"
|
request.acceptType = "application/json"
|
||||||
|
@ -64,8 +64,8 @@ struct EpisodeItemView: View {
|
||||||
didSet {
|
didSet {
|
||||||
if favorite == true {
|
if favorite == true {
|
||||||
let request = RestRequest(method: .post,
|
let request = RestRequest(method: .post,
|
||||||
url: (globalData.server?.baseURI ?? "") +
|
url: (globalData.server.baseURI ?? "") +
|
||||||
"/Users/\(globalData.user?.user_id ?? "")/FavoriteItems/\(fullItem.Id)")
|
"/Users/\(globalData.user.user_id ?? "")/FavoriteItems/\(fullItem.Id)")
|
||||||
request.headerParameters["X-Emby-Authorization"] = globalData.authHeader
|
request.headerParameters["X-Emby-Authorization"] = globalData.authHeader
|
||||||
request.contentType = "application/json"
|
request.contentType = "application/json"
|
||||||
request.acceptType = "application/json"
|
request.acceptType = "application/json"
|
||||||
|
@ -74,8 +74,8 @@ struct EpisodeItemView: View {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let request = RestRequest(method: .delete,
|
let request = RestRequest(method: .delete,
|
||||||
url: (globalData.server?.baseURI ?? "") +
|
url: (globalData.server.baseURI ?? "") +
|
||||||
"/Users/\(globalData.user?.user_id ?? "")/FavoriteItems/\(fullItem.Id)")
|
"/Users/\(globalData.user.user_id ?? "")/FavoriteItems/\(fullItem.Id)")
|
||||||
request.headerParameters["X-Emby-Authorization"] = globalData.authHeader
|
request.headerParameters["X-Emby-Authorization"] = globalData.authHeader
|
||||||
request.contentType = "application/json"
|
request.contentType = "application/json"
|
||||||
request.acceptType = "application/json"
|
request.acceptType = "application/json"
|
||||||
|
@ -96,9 +96,9 @@ struct EpisodeItemView: View {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_viewDidLoad.wrappedValue = true
|
_viewDidLoad.wrappedValue = true
|
||||||
let url = "/Users/\(globalData.user?.user_id ?? "")/Items/\(item.Id)"
|
let url = "/Users/\(globalData.user.user_id ?? "")/Items/\(item.Id)"
|
||||||
|
|
||||||
let request = RestRequest(method: .get, url: (globalData.server?.baseURI ?? "") + url)
|
let request = RestRequest(method: .get, url: (globalData.server.baseURI ?? "") + url)
|
||||||
request.headerParameters["X-Emby-Authorization"] = globalData.authHeader
|
request.headerParameters["X-Emby-Authorization"] = globalData.authHeader
|
||||||
request.contentType = "application/json"
|
request.contentType = "application/json"
|
||||||
request.acceptType = "application/json"
|
request.acceptType = "application/json"
|
||||||
|
@ -152,7 +152,7 @@ struct EpisodeItemView: View {
|
||||||
cast.Role = person["Role"].string ?? ""
|
cast.Role = person["Role"].string ?? ""
|
||||||
cast
|
cast
|
||||||
.Image =
|
.Image =
|
||||||
URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(cast.Id)/Images/Primary?maxHeight=250&quality=85&tag=\(imageTag)")!
|
URL(string: "\(globalData.server.baseURI ?? "")/Items/\(cast.Id)/Images/Primary?maxHeight=250&quality=85&tag=\(imageTag)")!
|
||||||
fullItem.Cast.append(cast)
|
fullItem.Cast.append(cast)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -203,7 +203,7 @@ struct EpisodeItemView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
var portraitHeaderView: some View {
|
var portraitHeaderView: some View {
|
||||||
LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.ParentBackdropItemId)/Images/Backdrop?maxWidth=550&quality=90&tag=\(fullItem.Backdrop)"))
|
LazyImage(source: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(fullItem.ParentBackdropItemId)/Images/Backdrop?maxWidth=550&quality=90&tag=\(fullItem.Backdrop)"))
|
||||||
.placeholderAndFailure {
|
.placeholderAndFailure {
|
||||||
Image(uiImage: UIImage(blurHash: fullItem
|
Image(uiImage: UIImage(blurHash: fullItem
|
||||||
.BackdropBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : fullItem
|
.BackdropBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : fullItem
|
||||||
|
@ -219,7 +219,7 @@ struct EpisodeItemView: View {
|
||||||
var portraitHeaderOverlayView: some View {
|
var portraitHeaderOverlayView: some View {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
HStack(alignment: .bottom, spacing: 12) {
|
HStack(alignment: .bottom, spacing: 12) {
|
||||||
LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.SeriesId ?? "")/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)"))
|
LazyImage(source: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(fullItem.SeriesId ?? "")/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)"))
|
||||||
.placeholderAndFailure {
|
.placeholderAndFailure {
|
||||||
Image(uiImage: UIImage(blurHash: fullItem
|
Image(uiImage: UIImage(blurHash: fullItem
|
||||||
.PosterBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" :
|
.PosterBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" :
|
||||||
|
@ -420,7 +420,7 @@ struct EpisodeItemView: View {
|
||||||
} else {
|
} else {
|
||||||
GeometryReader { geometry in
|
GeometryReader { geometry in
|
||||||
ZStack {
|
ZStack {
|
||||||
LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.ParentBackdropItemId)/Images/Backdrop?maxWidth=\(String(Int(geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing)))&quality=80&tag=\(fullItem.Backdrop)"))
|
LazyImage(source: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(fullItem.ParentBackdropItemId)/Images/Backdrop?maxWidth=\(String(Int(geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing)))&quality=80&tag=\(fullItem.Backdrop)"))
|
||||||
.placeholderAndFailure {
|
.placeholderAndFailure {
|
||||||
Image(uiImage: UIImage(blurHash: fullItem
|
Image(uiImage: UIImage(blurHash: fullItem
|
||||||
.BackdropBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : fullItem
|
.BackdropBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : fullItem
|
||||||
|
@ -441,7 +441,7 @@ struct EpisodeItemView: View {
|
||||||
.blur(radius: 2)
|
.blur(radius: 2)
|
||||||
HStack {
|
HStack {
|
||||||
VStack {
|
VStack {
|
||||||
LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.SeriesId ?? "")/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)"))
|
LazyImage(source: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(fullItem.SeriesId ?? "")/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)"))
|
||||||
.placeholderAndFailure {
|
.placeholderAndFailure {
|
||||||
Image(uiImage: UIImage(blurHash: fullItem
|
Image(uiImage: UIImage(blurHash: fullItem
|
||||||
.PosterBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" :
|
.PosterBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" :
|
||||||
|
|
|
@ -7,25 +7,29 @@
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Introspect
|
import Introspect
|
||||||
|
import JellyfinAPI
|
||||||
|
|
||||||
class ItemPlayback: ObservableObject {
|
//good lord the environmental modifiers ;P
|
||||||
@Published var shouldPlay: Bool = false;
|
|
||||||
@Published var itemToPlay: DetailItem = DetailItem();
|
class VideoPlayerItem: ObservableObject {
|
||||||
|
@Published var shouldShowPlayer: Bool = false;
|
||||||
|
@Published var itemToPlay: BaseItemDto = BaseItemDto();
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ItemView: View {
|
struct ItemView: View {
|
||||||
var item: ResumeItem;
|
private var item: BaseItemDto;
|
||||||
@StateObject private var playback: ItemPlayback = ItemPlayback()
|
@StateObject private var videoPlayerItem: VideoPlayerItem = VideoPlayerItem()
|
||||||
@State private var shouldShowLoadingView: Bool = false;
|
|
||||||
|
|
||||||
init(item: ResumeItem) {
|
@State private var isLoading: Bool = false; //This variable is only changed by the underlying VLC view.
|
||||||
|
|
||||||
|
init(item: BaseItemDto) {
|
||||||
self.item = item;
|
self.item = item;
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
if(playback.shouldPlay) {
|
if(videoPlayerItem.shouldShowPlayer) {
|
||||||
LoadingViewNoBlur(isShowing: $shouldShowLoadingView) {
|
LoadingViewNoBlur(isShowing: $isLoading) {
|
||||||
VLCPlayerWithControls(item: playback.itemToPlay, loadBinding: $shouldShowLoadingView, pBinding: _playback.projectedValue.shouldPlay)
|
VLCPlayerWithControls(item: playback.itemToPlay, loadBinding: $isLoading, pBinding: _videoPlayerItem.projectedValue.shouldShowPlayer)
|
||||||
}.navigationBarHidden(true)
|
}.navigationBarHidden(true)
|
||||||
.navigationBarBackButtonHidden(true)
|
.navigationBarBackButtonHidden(true)
|
||||||
.statusBar(hidden: true)
|
.statusBar(hidden: true)
|
||||||
|
@ -34,7 +38,6 @@ struct ItemView: View {
|
||||||
.edgesIgnoringSafeArea(.all)
|
.edgesIgnoringSafeArea(.all)
|
||||||
.overrideViewPreference(.unspecified)
|
.overrideViewPreference(.unspecified)
|
||||||
.supportedOrientations(.landscape)
|
.supportedOrientations(.landscape)
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
Group {
|
Group {
|
||||||
if(item.Type == "Movie") {
|
if(item.Type == "Movie") {
|
||||||
|
@ -50,7 +53,7 @@ struct ItemView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.introspectTabBarController { (UITabBarController) in
|
.introspectTabBarController { (UITabBarController) in
|
||||||
UITabBarController.tabBar.isHidden = false
|
UITabBarController.tabBar.isHidden = false
|
||||||
}
|
}
|
||||||
.navigationBarHidden(false)
|
.navigationBarHidden(false)
|
||||||
.navigationBarBackButtonHidden(false)
|
.navigationBarBackButtonHidden(false)
|
||||||
|
|
|
@ -169,24 +169,6 @@ extension View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension String {
|
|
||||||
public func leftPad(toWidth width: Int, withString string: String?) -> String {
|
|
||||||
let paddingString = string ?? " "
|
|
||||||
|
|
||||||
if self.count >= width {
|
|
||||||
return self
|
|
||||||
}
|
|
||||||
|
|
||||||
let remainingLength: Int = width - self.count
|
|
||||||
var padString = String()
|
|
||||||
for _ in 0 ..< remainingLength {
|
|
||||||
padString += paddingString
|
|
||||||
}
|
|
||||||
|
|
||||||
return "\(padString)\(self)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@main
|
@main
|
||||||
struct JellyfinPlayerApp: App {
|
struct JellyfinPlayerApp: App {
|
||||||
let persistenceController = PersistenceController.shared
|
let persistenceController = PersistenceController.shared
|
||||||
|
|
|
@ -6,148 +6,68 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import SwiftyRequest
|
|
||||||
import SwiftyJSON
|
|
||||||
import NukeUI
|
import NukeUI
|
||||||
|
import JellyfinAPI
|
||||||
|
|
||||||
struct LatestMediaView: View {
|
struct LatestMediaView: View {
|
||||||
@Environment(\.managedObjectContext) private var viewContext
|
|
||||||
@EnvironmentObject var globalData: GlobalData
|
@EnvironmentObject var globalData: GlobalData
|
||||||
|
|
||||||
@State var resumeItems: [ResumeItem] = []
|
@State var items: [BaseItemDto] = []
|
||||||
private var library_id: String = "";
|
private var library_id: String = "";
|
||||||
@State private var viewDidLoad: Int = 0;
|
@State private var viewDidLoad: Bool = false;
|
||||||
|
|
||||||
init(library: String) {
|
init(usingLibraryID: String) {
|
||||||
library_id = library;
|
library_id = usingLibraryID;
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
library_id = "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func onAppear() {
|
func onAppear() {
|
||||||
if(globalData.server?.baseURI == "") {
|
if(viewDidLoad == true) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if(viewDidLoad == 1) {
|
viewDidLoad = true;
|
||||||
return
|
|
||||||
}
|
|
||||||
_viewDidLoad.wrappedValue = 1;
|
|
||||||
let request = RestRequest(method: .get, url: (globalData.server?.baseURI ?? "") + "/Users/\(globalData.user?.user_id ?? "")/Items/Latest?Limit=12&IncludeItemTypes=Movie%2CSeries&Limit=16&Fields=PrimaryImageAspectRatio%2CBasicSyncInfo%2CPath&ImageTypeLimit=1&EnableImageTypes=Primary%2CBackdrop%2CThumb&ParentId=\(library_id)")
|
|
||||||
request.headerParameters["X-Emby-Authorization"] = globalData.authHeader
|
|
||||||
request.contentType = "application/json"
|
|
||||||
request.acceptType = "application/json"
|
|
||||||
|
|
||||||
request.responseData() { (result: Result<RestResponse<Data>, RestError>) in
|
UserLibraryAPI.getLatestMedia(userId: globalData.user.user_id!, parentId: library_id, fields: [.primaryImageAspectRatio,.seriesPrimaryImage], enableUserData: true, limit: 12)
|
||||||
switch result {
|
.sink(receiveCompletion: { completion in
|
||||||
case .success(let response):
|
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
|
||||||
let body = response.body
|
}, receiveValue: { response in
|
||||||
do {
|
items = response
|
||||||
let json = try JSON(data: body)
|
})
|
||||||
for (_,item):(String, JSON) in json {
|
.store(in: &globalData.pendingAPIRequests)
|
||||||
// Do something you want
|
|
||||||
var itemObj = ResumeItem()
|
|
||||||
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
|
|
||||||
|
|
||||||
if(itemObj.Type == "Series") {
|
|
||||||
itemObj.ItemBadge = item["UserData"]["UnplayedItemCount"].int ?? 0
|
|
||||||
}
|
|
||||||
|
|
||||||
if(itemObj.Type != "Episode") {
|
|
||||||
_resumeItems.wrappedValue.append(itemObj)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//print("latestmediaview done https")
|
|
||||||
} catch {
|
|
||||||
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case .failure(let error):
|
|
||||||
debugPrint(error)
|
|
||||||
_viewDidLoad.wrappedValue = 0;
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView(.horizontal, showsIndicators: false) {
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
LazyHStack() {
|
LazyHStack() {
|
||||||
Spacer().frame(width:14)
|
Spacer().frame(width:16)
|
||||||
ForEach(resumeItems, id: \.Id) { item in
|
ForEach(items, id: \.id) { item in
|
||||||
NavigationLink(destination: ItemView(item: item)) {
|
if(item.type == "Series" || item.type == "Movie") {
|
||||||
VStack(alignment: .leading) {
|
NavigationLink(destination: EmptyView()) {
|
||||||
if(item.Type == "Series") {
|
VStack(alignment: .leading) {
|
||||||
Spacer().frame(height:10)
|
Spacer().frame(height:10)
|
||||||
LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?maxWidth=250&quality=80&tag=\(item.Image)"))
|
LazyImage(source: item.getPrimaryImage(baseURL: globalData.server.baseURI!, maxWidth: 100))
|
||||||
.placeholderAndFailure {
|
.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))!)
|
Image(uiImage: UIImage(blurHash: item.getPrimaryImageBlurHash(), size: CGSize(width: 16, height: 20))!)
|
||||||
.resizable()
|
.resizable()
|
||||||
.frame(width: 100, height: 150)
|
.frame(width: 100, height: 150)
|
||||||
.cornerRadius(10)
|
.cornerRadius(10)
|
||||||
}
|
}
|
||||||
.frame(width: 100, height: 150)
|
.frame(width: 100, height: 150)
|
||||||
.cornerRadius(10)
|
.cornerRadius(10)
|
||||||
.overlay(
|
Spacer().frame(height:5)
|
||||||
ZStack {
|
Text(item.seasonName ?? item.name ?? "")
|
||||||
if(item.ItemBadge == 0) {
|
.font(.caption)
|
||||||
Image(systemName: "checkmark")
|
.fontWeight(.semibold)
|
||||||
.font(.caption)
|
.foregroundColor(.primary)
|
||||||
.padding(3)
|
.lineLimit(1)
|
||||||
.foregroundColor(.white)
|
}.frame(width: 100)
|
||||||
} else {
|
Spacer().frame(width: 15)
|
||||||
Text("\(String(item.ItemBadge ?? 0))")
|
}
|
||||||
.font(.caption)
|
|
||||||
.padding(3)
|
|
||||||
.foregroundColor(.white)
|
|
||||||
}
|
|
||||||
}.background(Color.black)
|
|
||||||
.opacity(0.8)
|
|
||||||
.cornerRadius(10.0)
|
|
||||||
.padding(3), alignment: .topTrailing
|
|
||||||
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Spacer().frame(height:10)
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
Text(item.Name)
|
|
||||||
.font(.caption)
|
|
||||||
.fontWeight(.semibold)
|
|
||||||
.foregroundColor(.primary)
|
|
||||||
.lineLimit(1)
|
|
||||||
Spacer().frame(height:5)
|
|
||||||
}.frame(width: 100)
|
|
||||||
}
|
}
|
||||||
Spacer().frame(width: 14)
|
|
||||||
}
|
}
|
||||||
}.frame(height: 190)
|
}
|
||||||
}.onAppear(perform: onAppear).padding(EdgeInsets(top: -2, leading: 0, bottom: 0, trailing: 0)).frame(height: 190)
|
.frame(height: 190)
|
||||||
}
|
}
|
||||||
}
|
.onAppear(perform: onAppear)
|
||||||
|
.padding(EdgeInsets(top: -2, leading: 0, bottom: 0, trailing: 0)).frame(height: 190)
|
||||||
struct LatestMediaView_Previews: PreviewProvider {
|
|
||||||
static var previews: some View {
|
|
||||||
LatestMediaView()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,8 +65,8 @@ struct LibraryFilterView: View {
|
||||||
_sortOrder.wrappedValue = filter.asc?.rawValue ?? sortOrder
|
_sortOrder.wrappedValue = filter.asc?.rawValue ?? sortOrder
|
||||||
|
|
||||||
_allGenres.wrappedValue = []
|
_allGenres.wrappedValue = []
|
||||||
let url = "/Items/Filters?UserId=\(globalData.user?.user_id ?? "")&ParentId=\(library)"
|
let url = "/Items/Filters?UserId=\(globalData.user.user_id ?? "")&ParentId=\(library)"
|
||||||
let request = RestRequest(method: .get, url: (globalData.server?.baseURI ?? "") + url)
|
let request = RestRequest(method: .get, url: (globalData.server.baseURI ?? "") + url)
|
||||||
request.headerParameters["X-Emby-Authorization"] = globalData.authHeader
|
request.headerParameters["X-Emby-Authorization"] = globalData.authHeader
|
||||||
request.contentType = "application/json"
|
request.contentType = "application/json"
|
||||||
request.acceptType = "application/json"
|
request.acceptType = "application/json"
|
||||||
|
|
|
@ -87,7 +87,7 @@ struct ResumeItemGridCell: View {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
if item.Type == "Movie" {
|
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)"))
|
LazyImage(source: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?fillWidth=300&fillHeight=450&quality=90&tag=\(item.Image)"))
|
||||||
.placeholderAndFailure {
|
.placeholderAndFailure {
|
||||||
Image(uiImage: UIImage(blurHash: item
|
Image(uiImage: UIImage(blurHash: item
|
||||||
.BlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item
|
.BlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item
|
||||||
|
@ -100,7 +100,7 @@ struct ResumeItemGridCell: View {
|
||||||
.frame(width: 100, height: 150)
|
.frame(width: 100, height: 150)
|
||||||
.cornerRadius(10)
|
.cornerRadius(10)
|
||||||
} else {
|
} else {
|
||||||
LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?fillWidth=300&fillHeight=450&quality=90&tag=\(item.Image)"))
|
LazyImage(source: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?fillWidth=300&fillHeight=450&quality=90&tag=\(item.Image)"))
|
||||||
.placeholderAndFailure {
|
.placeholderAndFailure {
|
||||||
Image(uiImage: UIImage(blurHash: item
|
Image(uiImage: UIImage(blurHash: item
|
||||||
.BlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item
|
.BlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item
|
||||||
|
|
|
@ -136,7 +136,7 @@ extension LibraryView {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
if item.Type == "Movie" {
|
if item.Type == "Movie" {
|
||||||
LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?maxWidth=250&quality=80&tag=\(item.Image)"))
|
LazyImage(source: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?maxWidth=250&quality=80&tag=\(item.Image)"))
|
||||||
.placeholderAndFailure {
|
.placeholderAndFailure {
|
||||||
Image(uiImage: UIImage(blurHash: item
|
Image(uiImage: UIImage(blurHash: item
|
||||||
.BlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item
|
.BlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item
|
||||||
|
@ -149,7 +149,7 @@ extension LibraryView {
|
||||||
.frame(width: 100, height: 150)
|
.frame(width: 100, height: 150)
|
||||||
.cornerRadius(10)
|
.cornerRadius(10)
|
||||||
} else {
|
} else {
|
||||||
LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?maxWidth=250&quality=80&tag=\(item.Image)"))
|
LazyImage(source: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?maxWidth=250&quality=80&tag=\(item.Image)"))
|
||||||
.placeholderAndFailure {
|
.placeholderAndFailure {
|
||||||
Image(uiImage: UIImage(blurHash: item
|
Image(uiImage: UIImage(blurHash: item
|
||||||
.BlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item
|
.BlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item
|
||||||
|
|
|
@ -125,11 +125,11 @@ struct MovieItemView: View {
|
||||||
let formatter = DateFormatter()
|
let formatter = DateFormatter()
|
||||||
formatter.locale = Locale(identifier: "en_US_POSIX")
|
formatter.locale = Locale(identifier: "en_US_POSIX")
|
||||||
formatter.timeZone = TimeZone(secondsFromGMT: 0)
|
formatter.timeZone = TimeZone(secondsFromGMT: 0)
|
||||||
print((globalData.server?.baseURI ?? "") +
|
print((globalData.server.baseURI ?? "") +
|
||||||
"/Users/\(globalData.user?.user_id ?? "")/PlayedItems/\(fullItem.Id)?DatePlayed=\(formatter.string(from: date).replacingOccurrences(of: ":", with: "%3A"))")
|
"/Users/\(globalData.user.user_id ?? "")/PlayedItems/\(fullItem.Id)?DatePlayed=\(formatter.string(from: date).replacingOccurrences(of: ":", with: "%3A"))")
|
||||||
let request = RestRequest(method: .post,
|
let request = RestRequest(method: .post,
|
||||||
url: (globalData.server?.baseURI ?? "") +
|
url: (globalData.server.baseURI ?? "") +
|
||||||
"/Users/\(globalData.user?.user_id ?? "")/PlayedItems/\(fullItem.Id)?DatePlayed=\(formatter.string(from: date).replacingOccurrences(of: ":", with: "%3A"))")
|
"/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.headerParameters["X-Emby-Authorization"] = globalData.authHeader
|
||||||
request.contentType = "application/json"
|
request.contentType = "application/json"
|
||||||
request.acceptType = "application/json"
|
request.acceptType = "application/json"
|
||||||
|
@ -138,8 +138,8 @@ struct MovieItemView: View {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let request = RestRequest(method: .delete,
|
let request = RestRequest(method: .delete,
|
||||||
url: (globalData.server?.baseURI ?? "") +
|
url: (globalData.server.baseURI ?? "") +
|
||||||
"/Users/\(globalData.user?.user_id ?? "")/PlayedItems/\(fullItem.Id)")
|
"/Users/\(globalData.user.user_id ?? "")/PlayedItems/\(fullItem.Id)")
|
||||||
request.headerParameters["X-Emby-Authorization"] = globalData.authHeader
|
request.headerParameters["X-Emby-Authorization"] = globalData.authHeader
|
||||||
request.contentType = "application/json"
|
request.contentType = "application/json"
|
||||||
request.acceptType = "application/json"
|
request.acceptType = "application/json"
|
||||||
|
@ -155,8 +155,8 @@ struct MovieItemView: View {
|
||||||
didSet {
|
didSet {
|
||||||
if favorite == true {
|
if favorite == true {
|
||||||
let request = RestRequest(method: .post,
|
let request = RestRequest(method: .post,
|
||||||
url: (globalData.server?.baseURI ?? "") +
|
url: (globalData.server.baseURI ?? "") +
|
||||||
"/Users/\(globalData.user?.user_id ?? "")/FavoriteItems/\(fullItem.Id)")
|
"/Users/\(globalData.user.user_id ?? "")/FavoriteItems/\(fullItem.Id)")
|
||||||
request.headerParameters["X-Emby-Authorization"] = globalData.authHeader
|
request.headerParameters["X-Emby-Authorization"] = globalData.authHeader
|
||||||
request.contentType = "application/json"
|
request.contentType = "application/json"
|
||||||
request.acceptType = "application/json"
|
request.acceptType = "application/json"
|
||||||
|
@ -165,8 +165,8 @@ struct MovieItemView: View {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let request = RestRequest(method: .delete,
|
let request = RestRequest(method: .delete,
|
||||||
url: (globalData.server?.baseURI ?? "") +
|
url: (globalData.server.baseURI ?? "") +
|
||||||
"/Users/\(globalData.user?.user_id ?? "")/FavoriteItems/\(fullItem.Id)")
|
"/Users/\(globalData.user.user_id ?? "")/FavoriteItems/\(fullItem.Id)")
|
||||||
request.headerParameters["X-Emby-Authorization"] = globalData.authHeader
|
request.headerParameters["X-Emby-Authorization"] = globalData.authHeader
|
||||||
request.contentType = "application/json"
|
request.contentType = "application/json"
|
||||||
request.acceptType = "application/json"
|
request.acceptType = "application/json"
|
||||||
|
@ -187,9 +187,9 @@ struct MovieItemView: View {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_viewDidLoad.wrappedValue = true
|
_viewDidLoad.wrappedValue = true
|
||||||
let url = "/Users/\(globalData.user?.user_id ?? "")/Items/\(item.Id)"
|
let url = "/Users/\(globalData.user.user_id ?? "")/Items/\(item.Id)"
|
||||||
|
|
||||||
let request = RestRequest(method: .get, url: (globalData.server?.baseURI ?? "") + url)
|
let request = RestRequest(method: .get, url: (globalData.server.baseURI ?? "") + url)
|
||||||
request.headerParameters["X-Emby-Authorization"] = globalData.authHeader
|
request.headerParameters["X-Emby-Authorization"] = globalData.authHeader
|
||||||
request.contentType = "application/json"
|
request.contentType = "application/json"
|
||||||
request.acceptType = "application/json"
|
request.acceptType = "application/json"
|
||||||
|
@ -242,7 +242,7 @@ struct MovieItemView: View {
|
||||||
cast.Role = person["Role"].string ?? ""
|
cast.Role = person["Role"].string ?? ""
|
||||||
cast
|
cast
|
||||||
.Image =
|
.Image =
|
||||||
URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(cast.Id)/Images/Primary?maxWidth=250&quality=85&tag=\(imageTag)")!
|
URL(string: "\(globalData.server.baseURI ?? "")/Items/\(cast.Id)/Images/Primary?maxWidth=250&quality=85&tag=\(imageTag)")!
|
||||||
fullItem.Cast.append(cast)
|
fullItem.Cast.append(cast)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -293,7 +293,7 @@ struct MovieItemView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
var portraitHeaderView: some View {
|
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: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(fullItem.Id)/Images/Backdrop?maxWidth=550&quality=90&tag=\(fullItem.Backdrop)"))
|
||||||
.placeholderAndFailure {
|
.placeholderAndFailure {
|
||||||
Image(uiImage: UIImage(blurHash: fullItem
|
Image(uiImage: UIImage(blurHash: fullItem
|
||||||
.BackdropBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : fullItem
|
.BackdropBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : fullItem
|
||||||
|
@ -309,7 +309,7 @@ struct MovieItemView: View {
|
||||||
var portraitHeaderOverlayView: some View {
|
var portraitHeaderOverlayView: some View {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
HStack(alignment: .bottom, spacing: 12) {
|
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: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(fullItem.Id)/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)"))
|
||||||
.placeholderAndFailure {
|
.placeholderAndFailure {
|
||||||
Image(uiImage: UIImage(blurHash: fullItem
|
Image(uiImage: UIImage(blurHash: fullItem
|
||||||
.PosterBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" :
|
.PosterBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" :
|
||||||
|
@ -510,7 +510,7 @@ struct MovieItemView: View {
|
||||||
} else {
|
} else {
|
||||||
GeometryReader { geometry in
|
GeometryReader { geometry in
|
||||||
ZStack {
|
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)"))
|
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)"))
|
||||||
.placeholderAndFailure {
|
.placeholderAndFailure {
|
||||||
Image(uiImage: UIImage(blurHash: fullItem
|
Image(uiImage: UIImage(blurHash: fullItem
|
||||||
.BackdropBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : fullItem
|
.BackdropBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : fullItem
|
||||||
|
@ -531,7 +531,7 @@ struct MovieItemView: View {
|
||||||
.blur(radius: 2)
|
.blur(radius: 2)
|
||||||
HStack {
|
HStack {
|
||||||
VStack {
|
VStack {
|
||||||
LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.Id)/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)"))
|
LazyImage(source: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(fullItem.Id)/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)"))
|
||||||
.placeholderAndFailure {
|
.placeholderAndFailure {
|
||||||
Image(uiImage: UIImage(blurHash: fullItem
|
Image(uiImage: UIImage(blurHash: fullItem
|
||||||
.PosterBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" :
|
.PosterBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" :
|
||||||
|
|
|
@ -6,109 +6,73 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import SwiftyRequest
|
|
||||||
import SwiftyJSON
|
|
||||||
import NukeUI
|
import NukeUI
|
||||||
|
import JellyfinAPI
|
||||||
|
|
||||||
struct NextUpView: View {
|
struct NextUpView: View {
|
||||||
@Environment(\.managedObjectContext) private var viewContext
|
|
||||||
@EnvironmentObject var globalData: GlobalData
|
@EnvironmentObject var globalData: GlobalData
|
||||||
|
|
||||||
@State var resumeItems: [ResumeItem] = []
|
@State private var items: [BaseItemDto] = []
|
||||||
@State private var viewDidLoad: Int = 0;
|
@State private var viewDidLoad: Bool = false;
|
||||||
@State private var isLoading: Bool = false;
|
|
||||||
|
|
||||||
func onAppear() {
|
func onAppear() {
|
||||||
if(globalData.server?.baseURI == "") {
|
if(viewDidLoad == true) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if(viewDidLoad == 1) {
|
viewDidLoad = true;
|
||||||
return
|
|
||||||
}
|
|
||||||
_viewDidLoad.wrappedValue = 1;
|
|
||||||
let request = RestRequest(method: .get, url: (globalData.server?.baseURI ?? "") + "/Shows/NextUp?Limit=12&Recursive=true&Fields=PrimaryImageAspectRatio%2CBasicSyncInfo&ImageTypeLimit=1&EnableImageTypes=Primary%2CBackdrop%2CThumb&MediaTypes=Video&UserId=\(globalData.user?.user_id ?? "")")
|
|
||||||
request.headerParameters["X-Emby-Authorization"] = globalData.authHeader
|
|
||||||
request.contentType = "application/json"
|
|
||||||
request.acceptType = "application/json"
|
|
||||||
|
|
||||||
request.responseData() { (result: Result<RestResponse<Data>, RestError>) in
|
TvShowsAPI.getNextUp(userId: globalData.user.user_id!, limit: 12)
|
||||||
switch result {
|
.sink(receiveCompletion: { completion in
|
||||||
case .success(let response):
|
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
|
||||||
let body = response.body
|
}, receiveValue: { response in
|
||||||
do {
|
items = response.items ?? []
|
||||||
let json = try JSON(data: body)
|
})
|
||||||
for (_,item):(String, JSON) in json["Items"] {
|
.store(in: &globalData.pendingAPIRequests)
|
||||||
// Do something you want
|
|
||||||
var itemObj = ResumeItem()
|
|
||||||
itemObj.Image = item["SeriesPrimaryImageTag"].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
|
|
||||||
|
|
||||||
_resumeItems.wrappedValue.append(itemObj)
|
|
||||||
}
|
|
||||||
_isLoading.wrappedValue = false;
|
|
||||||
} catch {
|
|
||||||
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case .failure(let error):
|
|
||||||
debugPrint(error)
|
|
||||||
_viewDidLoad.wrappedValue = 0;
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
if(resumeItems.count != 0) {
|
if(items.count != 0) {
|
||||||
Text("Next Up").font(.title2).fontWeight(.bold).padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16))
|
Text("Next Up")
|
||||||
|
.font(.title2)
|
||||||
|
.fontWeight(.bold)
|
||||||
|
.padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16))
|
||||||
ScrollView(.horizontal, showsIndicators: false) {
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
LazyHStack() {
|
LazyHStack() {
|
||||||
if(isLoading == false) {
|
Spacer().frame(width:16)
|
||||||
Spacer().frame(width:14)
|
ForEach(items, id: \.id) { item in
|
||||||
ForEach(resumeItems, id: \.Id) { item in
|
NavigationLink(destination: EmptyView()) {
|
||||||
NavigationLink(destination: ItemView(item: item)) {
|
VStack(alignment: .leading) {
|
||||||
VStack(alignment: .leading) {
|
LazyImage(source: item.getSeriesPrimaryImage(baseURL: globalData.server.baseURI!, maxWidth: 100))
|
||||||
Spacer().frame(height:10)
|
.placeholderAndFailure {
|
||||||
LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.SeriesId ?? "")/Images/\(item.ImageType)?maxWidth=250&quality=80&tag=\(item.Image)"))
|
Image(uiImage: UIImage(blurHash: item.getSeriesPrimaryImageBlurHash(), size: CGSize(width: 16, height: 20))!)
|
||||||
.placeholderAndFailure {
|
.resizable()
|
||||||
Image(uiImage: UIImage(blurHash: (item.BlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item.BlurHash), size: CGSize(width: 16, height: 16))!)
|
.frame(width: 100, height: 150)
|
||||||
.resizable()
|
.cornerRadius(10)
|
||||||
.frame(width: 100, height: 150)
|
}
|
||||||
.cornerRadius(10)
|
.frame(width: 100, height: 150)
|
||||||
}
|
.cornerRadius(10)
|
||||||
.frame(width: 100, height: 150)
|
Spacer().frame(height:5)
|
||||||
.cornerRadius(10)
|
Text(item.seriesName!)
|
||||||
Text(item.SeriesName ?? "")
|
.font(.caption)
|
||||||
.font(.caption)
|
.fontWeight(.semibold)
|
||||||
.fontWeight(.semibold)
|
.foregroundColor(.primary)
|
||||||
.foregroundColor(.primary)
|
.lineLimit(1)
|
||||||
.lineLimit(1)
|
Text("S\(item.parentIndexNumber ?? 0):E\(item.indexNumber ?? 0)")
|
||||||
Text("S\(String(item.ParentIndexNumber ?? 0)):E\(String(item.IndexNumber ?? 0))")
|
.font(.caption)
|
||||||
.font(.caption)
|
.fontWeight(.semibold)
|
||||||
.fontWeight(.semibold)
|
.foregroundColor(.secondary)
|
||||||
.foregroundColor(.secondary)
|
.lineLimit(1)
|
||||||
.lineLimit(1)
|
}.frame(width: 100)
|
||||||
Spacer().frame(height:5)
|
Spacer().frame(width:16)
|
||||||
}
|
|
||||||
.frame(width: 100)
|
|
||||||
Spacer().frame(width:12)
|
|
||||||
}
|
|
||||||
Spacer().frame(width: 10)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.frame(height: 200)
|
}
|
||||||
}.padding(EdgeInsets(top: -2, leading: 0, bottom: 0, trailing: 0)).frame(height: 200)
|
}
|
||||||
|
.frame(height: 200)
|
||||||
}
|
}
|
||||||
}.onAppear(perform: onAppear).padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
|
}
|
||||||
|
.onAppear(perform: onAppear)
|
||||||
|
.padding(EdgeInsets(top: 4, leading: 0, bottom: 0, trailing: 0))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,9 +35,9 @@ struct SeasonItemView: View {
|
||||||
if hasAppearedOnce {
|
if hasAppearedOnce {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let url = "/Users/\(globalData.user?.user_id ?? "")/Items/\(item.Id)"
|
let url = "/Users/\(globalData.user.user_id ?? "")/Items/\(item.Id)"
|
||||||
|
|
||||||
let request = RestRequest(method: .get, url: (globalData.server?.baseURI ?? "") + url)
|
let request = RestRequest(method: .get, url: (globalData.server.baseURI ?? "") + url)
|
||||||
request.headerParameters["X-Emby-Authorization"] = globalData.authHeader
|
request.headerParameters["X-Emby-Authorization"] = globalData.authHeader
|
||||||
request.contentType = "application/json"
|
request.contentType = "application/json"
|
||||||
request.acceptType = "application/json"
|
request.acceptType = "application/json"
|
||||||
|
@ -84,7 +84,7 @@ struct SeasonItemView: View {
|
||||||
cast.Role = person["Role"].string ?? ""
|
cast.Role = person["Role"].string ?? ""
|
||||||
cast
|
cast
|
||||||
.Image =
|
.Image =
|
||||||
URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(cast.Id)/Images/Primary?maxWidth=2000&quality=90&tag=\(imageTag)")!
|
URL(string: "\(globalData.server.baseURI ?? "")/Items/\(cast.Id)/Images/Primary?maxWidth=2000&quality=90&tag=\(imageTag)")!
|
||||||
responseItem.Cast.append(cast)
|
responseItem.Cast.append(cast)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -92,8 +92,8 @@ struct SeasonItemView: View {
|
||||||
_fullItem.wrappedValue = responseItem
|
_fullItem.wrappedValue = responseItem
|
||||||
|
|
||||||
let url2 =
|
let url2 =
|
||||||
"/Shows/\(fullItem.SeriesId ?? "")/Episodes?SeasonId=\(item.Id)&UserId=\(globalData.user?.user_id ?? "")&Fields=ItemCounts%2CPrimaryImageAspectRatio%2CBasicSyncInfo%2CCanDelete%2CMediaSourceCount%2COverview"
|
"/Shows/\(fullItem.SeriesId ?? "")/Episodes?SeasonId=\(item.Id)&UserId=\(globalData.user.user_id ?? "")&Fields=ItemCounts%2CPrimaryImageAspectRatio%2CBasicSyncInfo%2CCanDelete%2CMediaSourceCount%2COverview"
|
||||||
let request2 = RestRequest(method: .get, url: (globalData.server?.baseURI ?? "") + url2)
|
let request2 = RestRequest(method: .get, url: (globalData.server.baseURI ?? "") + url2)
|
||||||
request2.headerParameters["X-Emby-Authorization"] = globalData.authHeader
|
request2.headerParameters["X-Emby-Authorization"] = globalData.authHeader
|
||||||
request2.contentType = "application/json"
|
request2.contentType = "application/json"
|
||||||
request2.acceptType = "application/json"
|
request2.acceptType = "application/json"
|
||||||
|
@ -193,7 +193,7 @@ struct SeasonItemView: View {
|
||||||
if isLoading {
|
if isLoading {
|
||||||
EmptyView()
|
EmptyView()
|
||||||
} else {
|
} else {
|
||||||
LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.SeriesId ?? "")/Images/Backdrop?maxWidth=550&quality=90&tag=\(item.SeasonImage ?? "")"))
|
LazyImage(source: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(fullItem.SeriesId ?? "")/Images/Backdrop?maxWidth=550&quality=90&tag=\(item.SeasonImage ?? "")"))
|
||||||
.placeholderAndFailure {
|
.placeholderAndFailure {
|
||||||
Image(uiImage: UIImage(blurHash: item
|
Image(uiImage: UIImage(blurHash: item
|
||||||
.SeasonImageBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item
|
.SeasonImageBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item
|
||||||
|
@ -209,7 +209,7 @@ struct SeasonItemView: View {
|
||||||
|
|
||||||
var portraitHeaderOverlayView: some View {
|
var portraitHeaderOverlayView: some View {
|
||||||
HStack(alignment: .bottom, spacing: 12) {
|
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: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(fullItem.Id)/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)"))
|
||||||
.placeholderAndFailure {
|
.placeholderAndFailure {
|
||||||
Image(uiImage: UIImage(blurHash: fullItem
|
Image(uiImage: UIImage(blurHash: fullItem
|
||||||
.PosterBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : fullItem
|
.PosterBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : fullItem
|
||||||
|
@ -258,7 +258,7 @@ struct SeasonItemView: View {
|
||||||
ForEach(episodes, id: \.Id) { episode in
|
ForEach(episodes, id: \.Id) { episode in
|
||||||
NavigationLink(destination: ItemView(item: episode.ResumeItem ?? ResumeItem())) {
|
NavigationLink(destination: ItemView(item: episode.ResumeItem ?? ResumeItem())) {
|
||||||
HStack {
|
HStack {
|
||||||
LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(episode.Id)/Images/Primary?maxWidth=300&quality=90&tag=\(episode.Poster)"))
|
LazyImage(source: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(episode.Id)/Images/Primary?maxWidth=300&quality=90&tag=\(episode.Poster)"))
|
||||||
.placeholderAndFailure {
|
.placeholderAndFailure {
|
||||||
Image(uiImage: UIImage(blurHash: episode
|
Image(uiImage: UIImage(blurHash: episode
|
||||||
.PosterBlurHash == "" ?
|
.PosterBlurHash == "" ?
|
||||||
|
@ -276,7 +276,7 @@ struct SeasonItemView: View {
|
||||||
.overlay(
|
.overlay(
|
||||||
Rectangle()
|
Rectangle()
|
||||||
.fill(Color(red: 172/255, green: 92/255, blue: 195/255))
|
.fill(Color(red: 172/255, green: 92/255, blue: 195/255))
|
||||||
.mask(CustomShape(radius: 10))
|
.mask(ProgressBar())
|
||||||
.frame(width: CGFloat((episode.Progress / Double(episode.RuntimeTicks)) * 150), height: 7)
|
.frame(width: CGFloat((episode.Progress / Double(episode.RuntimeTicks)) * 150), height: 7)
|
||||||
.padding(0), alignment: .bottomLeading
|
.padding(0), alignment: .bottomLeading
|
||||||
)
|
)
|
||||||
|
@ -333,7 +333,7 @@ struct SeasonItemView: View {
|
||||||
} else {
|
} else {
|
||||||
GeometryReader { geometry in
|
GeometryReader { geometry in
|
||||||
ZStack {
|
ZStack {
|
||||||
LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.SeriesId ?? "")/Images/Backdrop?maxWidth=\(String(Int(geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing)))&quality=80&tag=\(item.SeasonImage ?? "")"))
|
LazyImage(source: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(fullItem.SeriesId ?? "")/Images/Backdrop?maxWidth=\(String(Int(geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing)))&quality=80&tag=\(item.SeasonImage ?? "")"))
|
||||||
.placeholderAndFailure {
|
.placeholderAndFailure {
|
||||||
Image(uiImage: UIImage(blurHash: item
|
Image(uiImage: UIImage(blurHash: item
|
||||||
.SeasonImageBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item
|
.SeasonImageBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item
|
||||||
|
@ -355,7 +355,7 @@ struct SeasonItemView: View {
|
||||||
HStack {
|
HStack {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
Spacer().frame(height: 16)
|
Spacer().frame(height: 16)
|
||||||
LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.Id)/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)"))
|
LazyImage(source: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(fullItem.Id)/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)"))
|
||||||
.placeholderAndFailure {
|
.placeholderAndFailure {
|
||||||
Image(uiImage: UIImage(blurHash: fullItem
|
Image(uiImage: UIImage(blurHash: fullItem
|
||||||
.PosterBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" :
|
.PosterBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" :
|
||||||
|
@ -392,7 +392,7 @@ struct SeasonItemView: View {
|
||||||
ForEach(episodes, id: \.Id) { episode in
|
ForEach(episodes, id: \.Id) { episode in
|
||||||
NavigationLink(destination: ItemView(item: episode.ResumeItem ?? ResumeItem())) {
|
NavigationLink(destination: ItemView(item: episode.ResumeItem ?? ResumeItem())) {
|
||||||
HStack {
|
HStack {
|
||||||
LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(episode.Id)/Images/Primary?maxWidth=300&quality=90&tag=\(episode.Poster)"))
|
LazyImage(source: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(episode.Id)/Images/Primary?maxWidth=300&quality=90&tag=\(episode.Poster)"))
|
||||||
.placeholderAndFailure {
|
.placeholderAndFailure {
|
||||||
Image(uiImage: UIImage(blurHash: episode
|
Image(uiImage: UIImage(blurHash: episode
|
||||||
.PosterBlurHash == "" ?
|
.PosterBlurHash == "" ?
|
||||||
|
@ -410,7 +410,7 @@ struct SeasonItemView: View {
|
||||||
.overlay(
|
.overlay(
|
||||||
Rectangle()
|
Rectangle()
|
||||||
.fill(Color(red: 172/255, green: 92/255, blue: 195/255))
|
.fill(Color(red: 172/255, green: 92/255, blue: 195/255))
|
||||||
.mask(CustomShape(radius: 10))
|
.mask(ProgressBar())
|
||||||
.frame(width: CGFloat((episode.Progress / Double(episode.RuntimeTicks)) * 150), height: 7)
|
.frame(width: CGFloat((episode.Progress / Double(episode.RuntimeTicks)) * 150), height: 7)
|
||||||
.padding(0), alignment: .bottomLeading
|
.padding(0), alignment: .bottomLeading
|
||||||
)
|
)
|
||||||
|
|
|
@ -22,9 +22,9 @@ struct SeriesItemView: View {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_isLoading.wrappedValue = true;
|
_isLoading.wrappedValue = true;
|
||||||
let url = "/Shows/\(item.Id )/Seasons?userId=\(globalData.user?.user_id ?? "")&Fields=ItemCount"
|
let url = "/Shows/\(item.Id )/Seasons?userId=\(globalData.user.user_id ?? "")&Fields=ItemCount"
|
||||||
|
|
||||||
let request = RestRequest(method: .get, url: (globalData.server?.baseURI ?? "") + url)
|
let request = RestRequest(method: .get, url: (globalData.server.baseURI ?? "") + url)
|
||||||
request.headerParameters["X-Emby-Authorization"] = globalData.authHeader
|
request.headerParameters["X-Emby-Authorization"] = globalData.authHeader
|
||||||
request.contentType = "application/json"
|
request.contentType = "application/json"
|
||||||
request.acceptType = "application/json"
|
request.acceptType = "application/json"
|
||||||
|
@ -97,7 +97,7 @@ struct SeriesItemView: View {
|
||||||
ForEach(items, id: \.Id) { item in
|
ForEach(items, id: \.Id) { item in
|
||||||
NavigationLink(destination: ItemView(item: item )) {
|
NavigationLink(destination: ItemView(item: item )) {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?maxWidth=250&quality=90&tag=\(item.Image)"))
|
LazyImage(source: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?maxWidth=250&quality=90&tag=\(item.Image)"))
|
||||||
.placeholderAndFailure {
|
.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))!)
|
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()
|
.resizable()
|
||||||
|
|
|
@ -32,7 +32,7 @@ struct SettingsView: View {
|
||||||
private var autoSelectSubtitlesLangcode: String = "none"
|
private var autoSelectSubtitlesLangcode: String = "none"
|
||||||
|
|
||||||
func onAppear() {
|
func onAppear() {
|
||||||
_username.wrappedValue = globalData.user?.username ?? ""
|
_username.wrappedValue = globalData.user.username ?? ""
|
||||||
let defaults = UserDefaults.standard
|
let defaults = UserDefaults.standard
|
||||||
_inNetworkStreamBitrate.wrappedValue = defaults.integer(forKey: "InNetworkBandwidth")
|
_inNetworkStreamBitrate.wrappedValue = defaults.integer(forKey: "InNetworkBandwidth")
|
||||||
_outOfNetworkStreamBitrate.wrappedValue = defaults.integer(forKey: "OutOfNetworkBandwidth")
|
_outOfNetworkStreamBitrate.wrappedValue = defaults.integer(forKey: "OutOfNetworkBandwidth")
|
||||||
|
@ -94,8 +94,8 @@ struct SettingsView: View {
|
||||||
// TODO: handle the error
|
// TODO: handle the error
|
||||||
}
|
}
|
||||||
|
|
||||||
globalData.server = nil
|
globalData.server = Server()
|
||||||
globalData.user = nil
|
globalData.user = SignedInUser()
|
||||||
globalData.authToken = ""
|
globalData.authToken = ""
|
||||||
globalData.authHeader = ""
|
globalData.authHeader = ""
|
||||||
jsi.did = true
|
jsi.did = true
|
||||||
|
|
|
@ -7,19 +7,13 @@
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import MobileVLCKit
|
import MobileVLCKit
|
||||||
import SwiftyJSON
|
import JellyfinAPI
|
||||||
import SwiftyRequest
|
|
||||||
|
|
||||||
enum VideoType {
|
|
||||||
case hls;
|
|
||||||
case direct;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Subtitle {
|
struct Subtitle {
|
||||||
var name: String;
|
var name: String;
|
||||||
var id: Int32;
|
var id: Int32;
|
||||||
var url: URL;
|
var url: URL;
|
||||||
var delivery: String;
|
var delivery: SubtitleDeliveryMethod;
|
||||||
var codec: String;
|
var codec: String;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,7 +23,7 @@ struct AudioTrack {
|
||||||
}
|
}
|
||||||
|
|
||||||
class PlaybackItem: ObservableObject {
|
class PlaybackItem: ObservableObject {
|
||||||
@Published var videoType: VideoType = .hls;
|
@Published var videoType: PlayMethod = .directPlay;
|
||||||
@Published var videoUrl: URL = URL(string: "https://example.com")!;
|
@Published var videoUrl: URL = URL(string: "https://example.com")!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,7 +75,7 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
|
||||||
var subtitleTrackArray: [Subtitle] = [];
|
var subtitleTrackArray: [Subtitle] = [];
|
||||||
var audioTrackArray: [AudioTrack] = [];
|
var audioTrackArray: [AudioTrack] = [];
|
||||||
|
|
||||||
var manifest: DetailItem = DetailItem();
|
var manifest: BaseItemDto = BaseItemDto();
|
||||||
var playbackItem = PlaybackItem();
|
var playbackItem = PlaybackItem();
|
||||||
|
|
||||||
@IBAction func seekSliderStart(_ sender: Any) {
|
@IBAction func seekSliderStart(_ sender: Any) {
|
||||||
|
@ -199,8 +193,6 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
|
||||||
//Rotate to landscape only if necessary
|
//Rotate to landscape only if necessary
|
||||||
UIViewController.attemptRotationToDeviceOrientation();
|
UIViewController.attemptRotationToDeviceOrientation();
|
||||||
|
|
||||||
//Show loading screen
|
|
||||||
|
|
||||||
mediaPlayer.perform(Selector(("setTextRendererFontSize:")), with: 14)
|
mediaPlayer.perform(Selector(("setTextRendererFontSize:")), with: 14)
|
||||||
//mediaPlayer.wrappedValue.perform(Selector(("setTextRendererFont:")), with: "Copperplate")
|
//mediaPlayer.wrappedValue.perform(Selector(("setTextRendererFont:")), with: "Copperplate")
|
||||||
|
|
||||||
|
@ -208,7 +200,7 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
|
||||||
mediaPlayer.delegate = self
|
mediaPlayer.delegate = self
|
||||||
mediaPlayer.drawable = videoContentView
|
mediaPlayer.drawable = videoContentView
|
||||||
|
|
||||||
titleLabel.text = manifest.Name
|
titleLabel.text = manifest.name
|
||||||
|
|
||||||
//Fetch max bitrate from UserDefaults depending on current connection mode
|
//Fetch max bitrate from UserDefaults depending on current connection mode
|
||||||
let defaults = UserDefaults.standard
|
let defaults = UserDefaults.standard
|
||||||
|
@ -219,118 +211,88 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
|
||||||
builder.setMaxBitrate(bitrate: maxBitrate)
|
builder.setMaxBitrate(bitrate: maxBitrate)
|
||||||
let profile = builder.buildProfile()
|
let profile = builder.buildProfile()
|
||||||
|
|
||||||
let jsonEncoder = JSONEncoder()
|
let playbackInfo = PlaybackInfoDto(userId: globalData.user.user_id, maxStreamingBitrate: Int(maxBitrate), startTimeTicks: manifest.userData?.playbackPositionTicks ?? 0, deviceProfile: profile, autoOpenLiveStream: true)
|
||||||
let jsonData = try! jsonEncoder.encode(profile)
|
|
||||||
|
|
||||||
let url = (globalData.server?.baseURI ?? "") + "/Items/\(manifest.Id)/PlaybackInfo?UserId=\(globalData.user?.user_id ?? "")&StartTimeTicks=\(Int(manifest.Progress))&IsPlayback=true&AutoOpenLiveStream=true&MaxStreamingBitrate=\(profile.DeviceProfile.MaxStreamingBitrate)";
|
MediaInfoAPI.getPostedPlaybackInfo(itemId: manifest.id!, userId: globalData.user.user_id, maxStreamingBitrate: Int(maxBitrate), startTimeTicks: manifest.userData?.playbackPositionTicks ?? 0, autoOpenLiveStream: true, playbackInfoDto: playbackInfo)
|
||||||
|
.sink(receiveCompletion: { completion in
|
||||||
let request = RestRequest(method: .post, url: url)
|
HandleAPIRequestCompletion(globalData: self.globalData, completion: completion)
|
||||||
|
}, receiveValue: { [self] response in
|
||||||
request.headerParameters["X-Emby-Authorization"] = globalData.authHeader
|
playSessionId = response.playSessionId!
|
||||||
request.contentType = "application/json"
|
let mediaSource = response.mediaSources!.first.self!
|
||||||
request.acceptType = "application/json"
|
if(mediaSource.transcodingUrl != nil) {
|
||||||
request.messageBody = jsonData
|
//Item is being transcoded by request of server
|
||||||
|
let streamURL = URL(string: "\(globalData.server.baseURI!)\(mediaSource.transcodingUrl!)")
|
||||||
request.responseData() { [self] (result: Result<RestResponse<Data>, RestError>) in
|
let item = PlaybackItem()
|
||||||
switch result {
|
item.videoType = .transcode
|
||||||
case .success(let response):
|
item.videoUrl = streamURL!
|
||||||
let body = response.body
|
|
||||||
do {
|
let disableSubtitleTrack = Subtitle(name: "Disabled", id: -1, url: URL(string: "https://example.com")!, delivery: .embed, codec: "")
|
||||||
let json = try JSON(data: body)
|
subtitleTrackArray.append(disableSubtitleTrack);
|
||||||
playSessionId = json["PlaySessionId"].string ?? "";
|
|
||||||
if(json["MediaSources"][0]["TranscodingUrl"].string != nil) {
|
//Loop through media streams and add to array
|
||||||
let streamURL: URL = URL(string: "\(globalData.server?.baseURI ?? "")\((json["MediaSources"][0]["TranscodingUrl"].string ?? ""))")!
|
for stream in mediaSource.mediaStreams! {
|
||||||
let item = PlaybackItem()
|
if(stream.type == .subtitle) {
|
||||||
item.videoType = .hls
|
let deliveryUrl = URL(string: "\(globalData.server.baseURI!)\(stream.deliveryUrl!)")!
|
||||||
item.videoUrl = streamURL
|
let subtitle = Subtitle(name: stream.displayTitle!, id: Int32(stream.index!), url: deliveryUrl, delivery: stream.deliveryMethod!, codec: stream.codec!)
|
||||||
let disableSubtitleTrack = Subtitle(name: "Disabled", id: -1, url: URL(string: "https://example.com")!, delivery: "Embed", codec: "")
|
subtitleTrackArray.append(subtitle);
|
||||||
subtitleTrackArray.append(disableSubtitleTrack);
|
|
||||||
|
|
||||||
for (_,stream):(String, JSON) in json["MediaSources"][0]["MediaStreams"] {
|
|
||||||
if(stream["Type"].string == "Subtitle") { //ignore ripped subtitles - we don't want to extract subtitles
|
|
||||||
let deliveryUrl = URL(string: "\(globalData.server?.baseURI ?? "")\(stream["DeliveryUrl"].string ?? "")")!
|
|
||||||
let subtitle = Subtitle(name: stream["DisplayTitle"].string ?? "", id: Int32(stream["Index"].int ?? 0), url: deliveryUrl, delivery: stream["DeliveryMethod"].string ?? "", codec: stream["Codec"].string ?? "")
|
|
||||||
subtitleTrackArray.append(subtitle);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(stream["Type"].string == "Audio") {
|
|
||||||
let subtitle = AudioTrack(name: stream["DisplayTitle"].string ?? "", id: Int32(stream["Index"].int ?? 0))
|
|
||||||
if(stream["IsDefault"].boolValue) {
|
|
||||||
selectedAudioTrack = Int32(stream["Index"].int ?? 0);
|
|
||||||
}
|
|
||||||
audioTrackArray.append(subtitle);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if(selectedAudioTrack == -1) {
|
if(stream.type == .audio) {
|
||||||
if(audioTrackArray.count > 0) {
|
let subtitle = AudioTrack(name: stream.displayTitle!, id: Int32(stream.index!))
|
||||||
selectedAudioTrack = audioTrackArray[0].id;
|
if(stream.isDefault! == true) {
|
||||||
|
selectedAudioTrack = Int32(stream.index!);
|
||||||
}
|
}
|
||||||
|
audioTrackArray.append(subtitle);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.sendPlayReport()
|
|
||||||
playbackItem = item;
|
|
||||||
} else {
|
|
||||||
print("Direct playing!");
|
|
||||||
let streamURL: URL = URL(string: "\(globalData.server?.baseURI ?? "")/Videos/\(manifest.Id)/stream?Static=true&mediaSourceId=\(manifest.Id)&deviceId=\(globalData.user?.device_uuid ?? "")&api_key=\(globalData.authToken)&Tag=\(json["MediaSources"][0]["ETag"])")!;
|
|
||||||
let item = PlaybackItem()
|
|
||||||
item.videoUrl = streamURL
|
|
||||||
item.videoType = .direct
|
|
||||||
|
|
||||||
let disableSubtitleTrack = Subtitle(name: "Disabled", id: -1, url: URL(string: "https://example.com")!, delivery: "Embed", codec: "")
|
|
||||||
subtitleTrackArray.append(disableSubtitleTrack);
|
|
||||||
for (_,stream):(String, JSON) in json["MediaSources"][0]["MediaStreams"] {
|
|
||||||
if(stream["Type"].string == "Subtitle") {
|
|
||||||
let deliveryUrl = URL(string: "\(globalData.server?.baseURI ?? "")\(stream["DeliveryUrl"].string ?? "")")!
|
|
||||||
let subtitle = Subtitle(name: stream["DisplayTitle"].string ?? "", id: Int32(stream["Index"].int ?? 0), url: deliveryUrl, delivery: stream["DeliveryMethod"].string ?? "", codec: stream["Codec"].string ?? "")
|
|
||||||
subtitleTrackArray.append(subtitle);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(stream["Type"].string == "Audio") {
|
|
||||||
let subtitle = AudioTrack(name: stream["DisplayTitle"].string ?? "", id: Int32(stream["Index"].int ?? 0))
|
|
||||||
if(stream["IsDefault"].boolValue) {
|
|
||||||
selectedAudioTrack = Int32(stream["Index"].int ?? 0);
|
|
||||||
}
|
|
||||||
audioTrackArray.append(subtitle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(selectedAudioTrack == -1) {
|
|
||||||
if(audioTrackArray.count > 0) {
|
|
||||||
selectedAudioTrack = audioTrackArray[0].id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sendPlayReport()
|
|
||||||
playbackItem = item;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
DispatchQueue.global(qos: .background).async {
|
if(selectedAudioTrack == -1) {
|
||||||
mediaPlayer.media = VLCMedia(url: playbackItem.videoUrl)
|
if(audioTrackArray.count > 0) {
|
||||||
mediaPlayer.play()
|
selectedAudioTrack = audioTrackArray[0].id;
|
||||||
mediaPlayer.jumpForward(Int32(manifest.Progress/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()
|
|
||||||
}
|
}
|
||||||
} catch {
|
|
||||||
|
|
||||||
|
self.sendPlayReport()
|
||||||
|
playbackItem = item;
|
||||||
|
} else {
|
||||||
|
//Item will be directly played by the client.
|
||||||
|
let streamURL: URL = URL(string: "\(globalData.server.baseURI!)/Videos/\(manifest.id!)/stream?Static=true&mediaSourceId=\(manifest.id!)&deviceId=\(globalData.user.device_uuid!)&api_key=\(globalData.authToken)&Tag=\(mediaSource.eTag!)")!;
|
||||||
|
|
||||||
|
let item = PlaybackItem()
|
||||||
|
item.videoUrl = streamURL
|
||||||
|
item.videoType = .directPlay
|
||||||
|
|
||||||
|
let disableSubtitleTrack = Subtitle(name: "Disabled", id: -1, url: URL(string: "https://example.com")!, delivery: .embed, codec: "")
|
||||||
|
subtitleTrackArray.append(disableSubtitleTrack);
|
||||||
|
|
||||||
|
//Loop through media streams and add to array
|
||||||
|
for stream in mediaSource.mediaStreams! {
|
||||||
|
if(stream.type == .subtitle) {
|
||||||
|
let deliveryUrl = URL(string: "\(globalData.server.baseURI!)\(stream.deliveryUrl!)")!
|
||||||
|
let subtitle = Subtitle(name: stream.displayTitle!, id: Int32(stream.index!), url: deliveryUrl, delivery: stream.deliveryMethod!, codec: stream.codec!)
|
||||||
|
subtitleTrackArray.append(subtitle);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(stream.type == .audio) {
|
||||||
|
let subtitle = AudioTrack(name: stream.displayTitle!, id: Int32(stream.index!))
|
||||||
|
if(stream.isDefault! == true) {
|
||||||
|
selectedAudioTrack = Int32(stream.index!);
|
||||||
|
}
|
||||||
|
audioTrackArray.append(subtitle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(selectedAudioTrack == -1) {
|
||||||
|
if(audioTrackArray.count > 0) {
|
||||||
|
selectedAudioTrack = audioTrackArray[0].id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.sendPlayReport()
|
||||||
|
playbackItem = item;
|
||||||
}
|
}
|
||||||
break
|
})
|
||||||
case .failure(let error):
|
.store(in: &globalData.pendingAPIRequests)
|
||||||
debugPrint(error)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewWillAppear(_ animated: Bool) {
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
@ -430,74 +392,46 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
|
||||||
|
|
||||||
//MARK: Jellyfin Playstate updates
|
//MARK: Jellyfin Playstate updates
|
||||||
func sendProgressReport(eventName: String) {
|
func sendProgressReport(eventName: String) {
|
||||||
var progressBody: String = "";
|
let progressInfo = PlaybackProgressInfo(canSeek: true, item: manifest, itemId: manifest.id, sessionId: playSessionId, mediaSourceId: manifest.id, audioStreamIndex: Int(selectedAudioTrack), subtitleStreamIndex: Int(selectedCaptionTrack), isPaused: (mediaPlayer.state == .paused), isMuted: false, positionTicks: Int64(mediaPlayer.position * Float(manifest.runTimeTicks!)), playbackStartTimeTicks: Int64(startTime), volumeLevel: 100, brightness: 100, aspectRatio: nil, playMethod: playbackItem.videoType, liveStreamId: nil, playSessionId: playSessionId, repeatMode: .repeatNone, nowPlayingQueue: [], playlistItemId: "playlistItem0")
|
||||||
progressBody = "{\"VolumeLevel\":100,\"IsMuted\":false,\"IsPaused\":\(mediaPlayer.state == .paused ? "true" : "false"),\"RepeatMode\":\"RepeatNone\",\"ShuffleMode\":\"Sorted\",\"MaxStreamingBitrate\":120000000,\"PositionTicks\":\(Int(mediaPlayer.position * Float(manifest.RuntimeTicks))),\"PlaybackStartTimeTicks\":\(startTime),\"AudioStreamIndex\":\(selectedAudioTrack),\"BufferedRanges\":[{\"start\":0,\"end\":569735888.888889}],\"PlayMethod\":\"\(playbackItem.videoType == .hls ? "Transcode" : "DirectStream")\",\"PlaySessionId\":\"\(playSessionId)\",\"PlaylistItemId\":\"playlistItem0\",\"MediaSourceId\":\"\(manifest.Id)\",\"CanSeek\":true,\"ItemId\":\"\(manifest.Id)\",\"EventName\":\"\(eventName)\"}";
|
|
||||||
|
|
||||||
let request = RestRequest(method: .post, url: (globalData.server?.baseURI ?? "") + "/Sessions/Playing/Progress")
|
PlaystateAPI.reportPlaybackProgress(playbackProgressInfo: progressInfo)
|
||||||
request.headerParameters["X-Emby-Authorization"] = globalData.authHeader
|
.sink(receiveCompletion: { completion in
|
||||||
request.contentType = "application/json"
|
HandleAPIRequestCompletion(globalData: self.globalData, completion: completion)
|
||||||
request.acceptType = "application/json"
|
}, receiveValue: { response in
|
||||||
request.messageBody = progressBody.data(using: .ascii);
|
print("Playback progress report sent!")
|
||||||
request.responseData() { (result: Result<RestResponse<Data>, RestError>) in
|
})
|
||||||
switch result {
|
.store(in: &globalData.pendingAPIRequests)
|
||||||
case .success(let resp):
|
|
||||||
print(resp.body)
|
|
||||||
break
|
|
||||||
case .failure(let error):
|
|
||||||
debugPrint(error)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendStopReport() {
|
func sendStopReport() {
|
||||||
var progressBody: String = "";
|
let stopInfo = PlaybackStopInfo(item: manifest, itemId: manifest.id, sessionId: playSessionId, mediaSourceId: manifest.id, positionTicks: Int64(mediaPlayer.position * Float(manifest.runTimeTicks!)), liveStreamId: nil, playSessionId: playSessionId, failed: nil, nextMediaType: nil, playlistItemId: "playlistItem0", nowPlayingQueue: [])
|
||||||
|
|
||||||
progressBody = "{\"VolumeLevel\":100,\"IsMuted\":false,\"IsPaused\":true,\"RepeatMode\":\"RepeatNone\",\"ShuffleMode\":\"Sorted\",\"MaxStreamingBitrate\":120000000,\"PositionTicks\":\(Int(mediaPlayer.position * Float(manifest.RuntimeTicks))),\"PlaybackStartTimeTicks\":\(startTime),\"AudioStreamIndex\":\(selectedAudioTrack),\"BufferedRanges\":[{\"start\":0,\"end\":100000}],\"PlayMethod\":\"\(playbackItem.videoType == .hls ? "Transcode" : "DirectStream")\",\"PlaySessionId\":\"\(playSessionId)\",\"PlaylistItemId\":\"playlistItem0\",\"MediaSourceId\":\"\(manifest.Id)\",\"CanSeek\":true,\"ItemId\":\"\(manifest.Id)\",\"NowPlayingQueue\":[{\"Id\":\"\(manifest.Id)\",\"PlaylistItemId\":\"playlistItem0\"}]}";
|
PlaystateAPI.reportPlaybackStopped(playbackStopInfo: stopInfo)
|
||||||
|
.sink(receiveCompletion: { completion in
|
||||||
let request = RestRequest(method: .post, url: (globalData.server?.baseURI ?? "") + "/Sessions/Playing/Stopped")
|
HandleAPIRequestCompletion(globalData: self.globalData, completion: completion)
|
||||||
request.headerParameters["X-Emby-Authorization"] = globalData.authHeader
|
}, receiveValue: { response in
|
||||||
request.contentType = "application/json"
|
print("Playback stop report sent!")
|
||||||
request.acceptType = "application/json"
|
})
|
||||||
request.messageBody = progressBody.data(using: .ascii);
|
.store(in: &globalData.pendingAPIRequests)
|
||||||
request.responseData() { (result: Result<RestResponse<Data>, RestError>) in
|
|
||||||
switch result {
|
|
||||||
case .success(let resp):
|
|
||||||
print(resp.body)
|
|
||||||
break
|
|
||||||
case .failure(let error):
|
|
||||||
debugPrint(error)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendPlayReport() {
|
func sendPlayReport() {
|
||||||
var progressBody: String = "";
|
|
||||||
startTime = Int(Date().timeIntervalSince1970) * 10000000
|
startTime = Int(Date().timeIntervalSince1970) * 10000000
|
||||||
|
|
||||||
progressBody = "{\"VolumeLevel\":100,\"IsMuted\":false,\"IsPaused\":false,\"RepeatMode\":\"RepeatNone\",\"ShuffleMode\":\"Sorted\",\"MaxStreamingBitrate\":120000000,\"PositionTicks\":\(Int(manifest.Progress)),\"PlaybackStartTimeTicks\":\(startTime),\"AudioStreamIndex\":\(selectedAudioTrack),\"BufferedRanges\":[],\"PlayMethod\":\"\(playbackItem.videoType == .hls ? "Transcode" : "DirectStream")\",\"PlaySessionId\":\"\(playSessionId)\",\"PlaylistItemId\":\"playlistItem0\",\"MediaSourceId\":\"\(manifest.Id)\",\"CanSeek\":true,\"ItemId\":\"\(manifest.Id)\",\"NowPlayingQueue\":[{\"Id\":\"\(manifest.Id)\",\"PlaylistItemId\":\"playlistItem0\"}]}";
|
let startInfo = PlaybackStartInfo(canSeek: true, item: manifest, itemId: manifest.id, sessionId: playSessionId, mediaSourceId: manifest.id, audioStreamIndex: Int(selectedAudioTrack), subtitleStreamIndex: Int(selectedCaptionTrack), isPaused: false, isMuted: false, positionTicks: manifest.userData?.playbackPositionTicks, playbackStartTimeTicks: Int64(startTime), volumeLevel: 100, brightness: 100, aspectRatio: nil, playMethod: playbackItem.videoType, liveStreamId: nil, playSessionId: playSessionId, repeatMode: .repeatNone, nowPlayingQueue: [], playlistItemId: "playlistItem0")
|
||||||
|
|
||||||
let request = RestRequest(method: .post, url: (globalData.server?.baseURI ?? "") + "/Sessions/Playing")
|
PlaystateAPI.reportPlaybackStart(playbackStartInfo: startInfo)
|
||||||
request.headerParameters["X-Emby-Authorization"] = globalData.authHeader
|
.sink(receiveCompletion: { completion in
|
||||||
request.contentType = "application/json"
|
HandleAPIRequestCompletion(globalData: self.globalData, completion: completion)
|
||||||
request.acceptType = "application/json"
|
}, receiveValue: { response in
|
||||||
request.messageBody = progressBody.data(using: .ascii);
|
print("Playback start report sent!")
|
||||||
request.responseData() { (result: Result<RestResponse<Data>, RestError>) in
|
})
|
||||||
switch result {
|
.store(in: &globalData.pendingAPIRequests)
|
||||||
case .success(let resp):
|
|
||||||
print(resp.body)
|
|
||||||
break
|
|
||||||
case .failure(let error):
|
|
||||||
debugPrint(error)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct VLCPlayerWithControls: UIViewControllerRepresentable {
|
struct VLCPlayerWithControls: UIViewControllerRepresentable {
|
||||||
var item: DetailItem
|
var item: BaseItemDto
|
||||||
@Environment(\.presentationMode) var presentationMode
|
@Environment(\.presentationMode) var presentationMode
|
||||||
@EnvironmentObject private var globalData: GlobalData;
|
@EnvironmentObject private var globalData: GlobalData;
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,81 @@
|
||||||
|
/* SwiftFin is subject to the terms of the Mozilla Public
|
||||||
|
* License, v2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
*
|
||||||
|
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import JellyfinAPI
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
//001fC^ = dark grey plain blurhash
|
||||||
|
|
||||||
|
extension BaseItemDto {
|
||||||
|
func getSeriesPrimaryImageBlurHash() -> String {
|
||||||
|
let rawImgURL = self.getSeriesPrimaryImage(baseURL: "", maxWidth: 1).absoluteString;
|
||||||
|
let imgTag = rawImgURL.components(separatedBy: "&tag=")[1];
|
||||||
|
|
||||||
|
return self.imageBlurHashes?.primary?[imgTag] ?? "001fC^";
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPrimaryImageBlurHash() -> String {
|
||||||
|
let rawImgURL = self.getPrimaryImage(baseURL: "", maxWidth: 1).absoluteString;
|
||||||
|
let imgTag = rawImgURL.components(separatedBy: "&tag=")[1];
|
||||||
|
|
||||||
|
return self.imageBlurHashes?.primary?[imgTag] ?? "001fC^";
|
||||||
|
}
|
||||||
|
|
||||||
|
func getBackdropImageBlurHash() -> String {
|
||||||
|
let rawImgURL = self.getBackdropImage(baseURL: "", maxWidth: 1).absoluteString;
|
||||||
|
let imgTag = rawImgURL.components(separatedBy: "&tag=")[1];
|
||||||
|
|
||||||
|
if(rawImgURL.contains("Backdrop")) {
|
||||||
|
return self.imageBlurHashes?.backdrop?[imgTag] ?? "001fC^";
|
||||||
|
} else {
|
||||||
|
return self.imageBlurHashes?.primary?[imgTag] ?? "001fC^";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getBackdropImage(baseURL: String, maxWidth: Int) -> URL {
|
||||||
|
var imageType = "";
|
||||||
|
var imageTag = "";
|
||||||
|
|
||||||
|
if(self.primaryImageAspectRatio ?? 0.0 < 1.0) {
|
||||||
|
imageType = "Backdrop";
|
||||||
|
imageTag = (self.backdropImageTags ?? [""])[0]
|
||||||
|
} else {
|
||||||
|
imageType = "Primary";
|
||||||
|
imageTag = self.imageTags?["Primary"] ?? ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if(imageTag == "") {
|
||||||
|
imageType = "Backdrop";
|
||||||
|
imageTag = self.parentBackdropImageTags?[0] ?? ""
|
||||||
|
}
|
||||||
|
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 getSeriesPrimaryImage(baseURL: String, maxWidth: Int) -> URL {
|
||||||
|
let imageType = "Primary";
|
||||||
|
let imageTag = self.seriesPrimaryImageTag ?? ""
|
||||||
|
let x = UIScreen.main.nativeScale * CGFloat(maxWidth)
|
||||||
|
let urlString = "\(baseURL)/Items/\(self.seriesId ?? "")/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=85&tag=\(imageTag)"
|
||||||
|
return URL(string: urlString)!
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPrimaryImage(baseURL: String, maxWidth: Int) -> URL {
|
||||||
|
let imageType = "Primary";
|
||||||
|
var imageTag = self.imageTags?["Primary"] ?? "";
|
||||||
|
|
||||||
|
if(imageTag == "") {
|
||||||
|
imageTag = self.seriesPrimaryImageTag ?? ""
|
||||||
|
}
|
||||||
|
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)!
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,4 +15,21 @@ extension String {
|
||||||
return regex.stringByReplacingMatches(in: self, options: [], range: range, withTemplate: replaceWith)
|
return regex.stringByReplacingMatches(in: self, options: [], range: range, withTemplate: replaceWith)
|
||||||
} catch { return self }
|
} catch { return self }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func leftPad(toWidth width: Int, withString string: String?) -> String {
|
||||||
|
let paddingString = string ?? " "
|
||||||
|
|
||||||
|
if self.count >= width {
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
let remainingLength: Int = width - self.count
|
||||||
|
var padString = String()
|
||||||
|
for _ in 0 ..< remainingLength {
|
||||||
|
padString += paddingString
|
||||||
|
}
|
||||||
|
|
||||||
|
return "\(padString)\(self)"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="18154" systemVersion="20E232" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithCloudKit="YES" userDefinedModelVersionIdentifier="">
|
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="18154" systemVersion="20E241" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithCloudKit="YES" userDefinedModelVersionIdentifier="">
|
||||||
<entity name="Server" representedClassName="Server" syncable="YES" codeGenerationType="class">
|
<entity name="Server" representedClassName="Server" syncable="YES" codeGenerationType="class">
|
||||||
<attribute name="baseURI" optional="YES" attributeType="String"/>
|
<attribute name="baseURI" attributeType="String" defaultValueString=""/>
|
||||||
<attribute name="name" optional="YES" attributeType="String"/>
|
<attribute name="name" attributeType="String" defaultValueString=""/>
|
||||||
<attribute name="server_id" optional="YES" attributeType="String"/>
|
<attribute name="server_id" attributeType="String" defaultValueString=""/>
|
||||||
</entity>
|
</entity>
|
||||||
<entity name="SignedInUser" representedClassName="SignedInUser" syncable="YES" codeGenerationType="class">
|
<entity name="SignedInUser" representedClassName="SignedInUser" syncable="YES" codeGenerationType="class">
|
||||||
<attribute name="device_uuid" optional="YES" attributeType="String"/>
|
<attribute name="device_uuid" attributeType="String" defaultValueString=""/>
|
||||||
<attribute name="user_id" optional="YES" attributeType="String"/>
|
<attribute name="user_id" attributeType="String" defaultValueString=""/>
|
||||||
<attribute name="username" optional="YES" attributeType="String"/>
|
<attribute name="username" attributeType="String" defaultValueString=""/>
|
||||||
</entity>
|
</entity>
|
||||||
<elements>
|
<elements>
|
||||||
<element name="Server" positionX="-63" positionY="-9" width="128" height="74"/>
|
<element name="Server" positionX="-63" positionY="-9" width="128" height="74"/>
|
||||||
|
|
|
@ -13,9 +13,9 @@ class justSignedIn: ObservableObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
class GlobalData: ObservableObject {
|
class GlobalData: ObservableObject {
|
||||||
@Published var user: SignedInUser?
|
@Published var user: SignedInUser = SignedInUser()
|
||||||
@Published var authToken: String = ""
|
@Published var authToken: String = ""
|
||||||
@Published var server: Server?
|
@Published var server: Server = Server()
|
||||||
@Published var authHeader: String = ""
|
@Published var authHeader: String = ""
|
||||||
@Published var isInNetwork: Bool = true;
|
@Published var isInNetwork: Bool = true;
|
||||||
@Published var networkError: Bool = false;
|
@Published var networkError: Bool = false;
|
||||||
|
|
Loading…
Reference in New Issue