more to mvc & gen. client

This commit is contained in:
Aiden Vigue 2021-06-08 17:04:33 -07:00
parent 85d79a4774
commit 93ef16a46d
11 changed files with 174 additions and 326 deletions

View File

@ -50,6 +50,8 @@
53A089D0264DA9DA00D57806 /* MovieItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53A089CF264DA9DA00D57806 /* MovieItemView.swift */; };
53A431BD266B0FF20016769F /* JellyfinAPI in Frameworks */ = {isa = PBXBuildFile; productRef = 53A431BC266B0FF20016769F /* JellyfinAPI */; };
53A431BF266B0FFE0016769F /* JellyfinAPI in Frameworks */ = {isa = PBXBuildFile; productRef = 53A431BE266B0FFE0016769F /* JellyfinAPI */; };
53AD124D267029D60094A276 /* SeriesItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53987CA526572F0700E7EA70 /* SeriesItemView.swift */; };
53AD124E26702B8A0094A276 /* SeasonItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53987CA326572C1300E7EA70 /* SeasonItemView.swift */; };
53C4404E266C75C70049424C /* HandleAPIRequestCompletion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53C4404D266C75C70049424C /* HandleAPIRequestCompletion.swift */; };
53C4404F266C75C70049424C /* HandleAPIRequestCompletion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53C4404D266C75C70049424C /* HandleAPIRequestCompletion.swift */; };
53D5E3DD264B47EE00BADDC8 /* MobileVLCKit.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 53D5E3DC264B47EE00BADDC8 /* MobileVLCKit.xcframework */; };
@ -119,7 +121,7 @@
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>"; };
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 iOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "JellyfinPlayer iOS.app"; sourceTree = BUILT_PRODUCTS_DIR; };
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>"; };
5377CBF8263B596B003A4E83 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
@ -136,6 +138,7 @@
53987CA72657424A00E7EA70 /* EpisodeItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodeItemView.swift; sourceTree = "<group>"; };
539B2DA4263BA5B8007FF1A4 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
53A089CF264DA9DA00D57806 /* MovieItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieItemView.swift; sourceTree = "<group>"; };
53AD124C2670278D0094A276 /* JellyfinPlayer.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = JellyfinPlayer.entitlements; sourceTree = "<group>"; };
53C4404D266C75C70049424C /* HandleAPIRequestCompletion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandleAPIRequestCompletion.swift; sourceTree = "<group>"; };
53D5E3DA264B460200BADDC8 /* Cartfile */ = {isa = PBXFileReference; lastKnownFileType = text; path = Cartfile; sourceTree = "<group>"; };
53D5E3DC264B47EE00BADDC8 /* MobileVLCKit.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = MobileVLCKit.xcframework; path = Carthage/Build/MobileVLCKit.xcframework; sourceTree = "<group>"; };
@ -236,7 +239,7 @@
5377CBF2263B596A003A4E83 /* Products */ = {
isa = PBXGroup;
children = (
5377CBF1263B596A003A4E83 /* JellyfinPlayer.app */,
5377CBF1263B596A003A4E83 /* JellyfinPlayer iOS.app */,
535870602669D21600D05A09 /* JellyfinPlayer tvOS.app */,
);
name = Products;
@ -245,6 +248,7 @@
5377CBF3263B596A003A4E83 /* JellyfinPlayer */ = {
isa = PBXGroup;
children = (
53AD124C2670278D0094A276 /* JellyfinPlayer.entitlements */,
5377CBF8263B596B003A4E83 /* Assets.xcassets */,
5338F74D263B61370014BF09 /* ConnectToServerView.swift */,
5377CBF6263B596A003A4E83 /* ContentView.swift */,
@ -342,9 +346,9 @@
productReference = 535870602669D21600D05A09 /* JellyfinPlayer tvOS.app */;
productType = "com.apple.product-type.application";
};
5377CBF0263B596A003A4E83 /* JellyfinPlayer */ = {
5377CBF0263B596A003A4E83 /* JellyfinPlayer iOS */ = {
isa = PBXNativeTarget;
buildConfigurationList = 5377CC1B263B596B003A4E83 /* Build configuration list for PBXNativeTarget "JellyfinPlayer" */;
buildConfigurationList = 5377CC1B263B596B003A4E83 /* Build configuration list for PBXNativeTarget "JellyfinPlayer iOS" */;
buildPhases = (
5377CBED263B596A003A4E83 /* Sources */,
5377CBEE263B596A003A4E83 /* Frameworks */,
@ -356,7 +360,7 @@
);
dependencies = (
);
name = JellyfinPlayer;
name = "JellyfinPlayer iOS";
packageProductDependencies = (
5338F756263B7E2E0014BF09 /* KeychainSwift */,
53352570265EA0A0006CCA86 /* Introspect */,
@ -364,7 +368,7 @@
53A431BC266B0FF20016769F /* JellyfinAPI */,
);
productName = JellyfinPlayer;
productReference = 5377CBF1263B596A003A4E83 /* JellyfinPlayer.app */;
productReference = 5377CBF1263B596A003A4E83 /* JellyfinPlayer iOS.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
@ -406,7 +410,7 @@
projectDirPath = "";
projectRoot = "";
targets = (
5377CBF0263B596A003A4E83 /* JellyfinPlayer */,
5377CBF0263B596A003A4E83 /* JellyfinPlayer iOS */,
5358705F2669D21600D05A09 /* JellyfinPlayer tvOS */,
);
};
@ -467,6 +471,7 @@
53FF7F2A263CF3F500585C35 /* LatestMediaView.swift in Sources */,
5377CBFE263B596B003A4E83 /* PersistenceController.swift in Sources */,
5389276E263C25100035E14B /* ContinueWatchingView.swift in Sources */,
53AD124E26702B8A0094A276 /* SeasonItemView.swift in Sources */,
535BAE9F2649E569005FA86D /* ItemView.swift in Sources */,
6225FCCB2663841E00E067F6 /* ParallaxHeader.swift in Sources */,
53F8377D265FF67C00F456B3 /* VideoPlayerSettingsView.swift in Sources */,
@ -485,6 +490,7 @@
5389277C263CC3DB0035E14B /* BlurHashDecode.swift in Sources */,
539B2DA5263BA5B8007FF1A4 /* SettingsView.swift in Sources */,
5338F74E263B61370014BF09 /* ConnectToServerView.swift in Sources */,
53AD124D267029D60094A276 /* SeriesItemView.swift in Sources */,
5377CBF5263B596A003A4E83 /* JellyfinPlayerApp.swift in Sources */,
53EE24E6265060780068F029 /* LibrarySearchView.swift in Sources */,
53892772263C8C6F0035E14B /* LoadingView.swift in Sources */,
@ -513,7 +519,7 @@
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.0;
PRODUCT_BUNDLE_IDENTIFIER = me.vigue.swiftfin.tv;
PRODUCT_BUNDLE_IDENTIFIER = me.vigue.jellyfin;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = appletvos;
SWIFT_VERSION = 5.0;
@ -541,7 +547,7 @@
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.0;
PRODUCT_BUNDLE_IDENTIFIER = me.vigue.swiftfin.tv;
PRODUCT_BUNDLE_IDENTIFIER = me.vigue.jellyfin;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = appletvos;
SWIFT_VERSION = 5.0;
@ -749,7 +755,7 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
5377CC1B263B596B003A4E83 /* Build configuration list for PBXNativeTarget "JellyfinPlayer" */ = {
5377CC1B263B596B003A4E83 /* Build configuration list for PBXNativeTarget "JellyfinPlayer iOS" */ = {
isa = XCConfigurationList;
buildConfigurations = (
5377CC1C263B596B003A4E83 /* Debug */,

View File

@ -15,8 +15,8 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "5377CBF0263B596A003A4E83"
BuildableName = "JellyfinPlayer.app"
BlueprintName = "JellyfinPlayer"
BuildableName = "JellyfinPlayer iOS.app"
BlueprintName = "JellyfinPlayer iOS"
ReferencedContainer = "container:JellyfinPlayer.xcodeproj">
</BuildableReference>
</BuildActionEntry>
@ -45,8 +45,8 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "5377CBF0263B596A003A4E83"
BuildableName = "JellyfinPlayer.app"
BlueprintName = "JellyfinPlayer"
BuildableName = "JellyfinPlayer iOS.app"
BlueprintName = "JellyfinPlayer iOS"
ReferencedContainer = "container:JellyfinPlayer.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
@ -62,8 +62,8 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "5377CBF0263B596A003A4E83"
BuildableName = "JellyfinPlayer.app"
BlueprintName = "JellyfinPlayer"
BuildableName = "JellyfinPlayer iOS.app"
BlueprintName = "JellyfinPlayer iOS"
ReferencedContainer = "container:JellyfinPlayer.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>

View File

@ -35,6 +35,8 @@ struct ContentView: View {
@State private var showSettingsPopover: Bool = false
@State private var viewDidLoad: Bool = false
@State private var loadState: Int = 2
private var recentFilterSet: LibraryFilters = LibraryFilters(filters: [], sortOrder: [.descending], sortBy: ["DateCreated"])
func startup() {
if(viewDidLoad == true) {
@ -158,7 +160,7 @@ struct ContentView: View {
Spacer()
NavigationLink(destination: LazyView {
LibraryView(usingParentID: library_id,
title: library_names[library_id] ?? "")
title: library_names[library_id] ?? "", usingFilters: recentFilterSet)
}) {
Text("See All").font(.subheadline).fontWeight(.bold)
}

View File

@ -25,21 +25,31 @@ struct ItemView: View {
@StateObject private var videoPlayerItem: VideoPlayerItem = VideoPlayerItem()
@State private var videoIsLoading: Bool = false; //This variable is only changed by the underlying VLC view.
@State private var isLoading: Bool = false;
@State private var viewDidLoad: Bool = false;
init(item: BaseItemDto) {
self.item = item
}
func onAppear() {
isLoading = true;
UserLibraryAPI.getItem(userId: globalData.user.user_id!, itemId: item.id!)
.sink(receiveCompletion: { completion in
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
}, receiveValue: { response in
isLoading = false
fullItem = response
})
.store(in: &globalData.pendingAPIRequests)
if(viewDidLoad) {
return
}
if(item.type == "Movie" || item.type == "Episode") {
isLoading = true;
UserLibraryAPI.getItem(userId: globalData.user.user_id!, itemId: item.id!)
.sink(receiveCompletion: { completion in
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
}, receiveValue: { response in
isLoading = false
viewDidLoad = true
fullItem = response
})
.store(in: &globalData.pendingAPIRequests)
} else {
viewDidLoad = true
}
}
var body: some View {
@ -60,15 +70,14 @@ struct ItemView: View {
ProgressView()
} else {
VStack {
if(fullItem.type == "Movie") {
if(item.type == "Movie") {
MovieItemView(item: fullItem)
} else if(fullItem.type == "Season") {
} else if(item.type == "Season") {
EmptyView()
//SeasonItemView(item: fullItem)
} else if(fullItem.type == "Series") {
EmptyView()
//SeriesItemView(item: fullItem)
} else if(fullItem.type == "Episode") {
SeasonItemView(item: item)
} else if(item.type == "Series") {
SeriesItemView(item: item)
} else if(item.type == "Episode") {
EmptyView()
//EpisodeItemView(item: fullItem)
} else {

View File

@ -6,5 +6,7 @@
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.developer.coremedia.hls.low-latency</key>
<true/>
</dict>
</plist>

View File

@ -40,11 +40,11 @@ struct LatestMediaView: View {
LazyHStack() {
Spacer().frame(width:16)
ForEach(items, id: \.id) { item in
if(item.type == "Series" || item.type == "Movie") {
if(item.type == "Series" || item.type == "Movie" || item.type == "Episode") {
NavigationLink(destination: ItemView(item: item)) {
VStack(alignment: .leading) {
Spacer().frame(height:10)
LazyImage(source: item.getPrimaryImage(baseURL: globalData.server.baseURI!, maxWidth: 100))
LazyImage(source: (item.type != "Episode" ? item.getPrimaryImage(baseURL: globalData.server.baseURI!, maxWidth: 100) : item.getSeriesPrimaryImage(baseURL: globalData.server.baseURI!, maxWidth: 100)))
.placeholderAndFailure {
Image(uiImage: UIImage(blurHash: item.getPrimaryImageBlurHash(), size: CGSize(width: 16, height: 20))!)
.resizable()
@ -54,11 +54,24 @@ struct LatestMediaView: View {
.frame(width: 100, height: 150)
.cornerRadius(10)
Spacer().frame(height:5)
Text(item.seasonName ?? item.name ?? "")
Text(item.seriesName ?? item.name ?? "")
.font(.caption)
.fontWeight(.semibold)
.foregroundColor(.primary)
.lineLimit(1)
if(item.type == "Episode") {
Text("S\(String(item.parentIndexNumber ?? 0)):E\(String(item.indexNumber ?? 0))")
.font(.caption)
.fontWeight(.semibold)
.foregroundColor(.secondary)
.lineLimit(1)
} else {
Text(String(item.productionYear ?? 0))
.font(.caption)
.fontWeight(.semibold)
.foregroundColor(.secondary)
.lineLimit(1)
}
}.frame(width: 100)
Spacer().frame(width: 15)
}

View File

@ -14,6 +14,7 @@ struct LibraryListView: View {
@State var library_ids: [String] = ["favorites", "genres"]
@State var library_names: [String: String] = ["favorites": "Favorites", "genres": "Genres"]
var libraries: [String: String] = [:] //input libraries
var withFavorites: LibraryFilters = LibraryFilters(filters: [.isFavorite], sortOrder: [.descending], sortBy: ["SortName"])
init(libraries: [String: String]) {
self.libraries = libraries
@ -34,7 +35,7 @@ struct LibraryListView: View {
switch key {
case "favorites":
NavigationLink(destination: LazyView {
LibraryView(usingParentID: "", title: library_names[key] ?? "", filters: [.isFavorite])
LibraryView(usingParentID: "", title: library_names[key] ?? "", usingFilters: withFavorites)
}) {
Text(library_names[key] ?? "")
}

View File

@ -18,7 +18,7 @@ struct LibraryView: View {
var usingParentID: String = ""
var title: String = ""
var filters: [ItemFilter] = []
var filters: LibraryFilters = LibraryFilters()
var personId: String = ""
var genre: String = ""
var studio: String = ""
@ -31,10 +31,10 @@ struct LibraryView: View {
self.title = title
}
init(usingParentID: String, title: String, filters: [ItemFilter]) {
init(usingParentID: String, title: String, usingFilters: LibraryFilters) {
self.usingParentID = usingParentID
self.title = title
self.filters = filters
self.filters = usingFilters
}
init(withPerson: BaseItemPerson) {
@ -60,7 +60,7 @@ struct LibraryView: View {
isLoading = true
items = []
ItemsAPI.getItemsByUserId(userId: globalData.user.user_id!, startIndex: currentPage * 100, limit: 100, recursive: true, searchTerm: nil, sortOrder: [.ascending], parentId: (usingParentID != "" ? usingParentID : nil), fields: [.parentId,.primaryImageAspectRatio,.basicSyncInfo], includeItemTypes: ["Movie","Series"], filters: filters, enableUserData: true, personIds: (personId == "" ? nil : [personId]), studioIds: (studio == "" ? nil : [studio]), genreIds: (genre == "" ? nil : [genre]), enableImages: true)
ItemsAPI.getItemsByUserId(userId: globalData.user.user_id!, startIndex: currentPage * 100, limit: 100, recursive: true, searchTerm: nil, sortOrder: filters.sortOrder, parentId: (usingParentID != "" ? usingParentID : nil), fields: [.parentId,.primaryImageAspectRatio,.basicSyncInfo], includeItemTypes: ["Movie","Series"], filters: filters.filters, sortBy: filters.sortBy, enableUserData: true, personIds: (personId == "" ? nil : [personId]), studioIds: (studio == "" ? nil : [studio]), genreIds: (genre == "" ? nil : [genre]), enableImages: true)
.sink(receiveCompletion: { completion in
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
isLoading = false
@ -121,29 +121,36 @@ struct LibraryView: View {
}.onChange(of: orientationInfo.orientation) { _ in
recalcTracks()
}
HStack() {
Spacer()
if(totalPages > 1) {
HStack() {
Button {
currentPage = currentPage - 1
onAppear()
} label: {
Image(systemName: "chevron.left")
}.disabled(currentPage == 0)
Text("\(String(currentPage+1)) of \(String(totalPages))")
Button {
currentPage = currentPage + 1
onAppear()
} label: {
Image(systemName: "chevron.right")
}.disabled(currentPage > totalPages - 1)
Spacer()
HStack() {
Button {
currentPage = currentPage - 1
onAppear()
} label: {
Image(systemName: "chevron.left")
.font(.system(size: 25))
}.disabled(currentPage == 0)
Text("Page \(String(currentPage+1)) of \(String(totalPages))")
.font(.headline)
.fontWeight(.semibold)
Button {
currentPage = currentPage + 1
onAppear()
} label: {
Image(systemName: "chevron.right")
.font(.system(size: 25))
}.disabled(currentPage > totalPages - 1)
}
Spacer()
}
Spacer()
}
Spacer().frame(height: 16)
}
}
} else {
Text("No results found :(")
Text("No results.")
}
}
}

View File

@ -12,168 +12,27 @@ import JellyfinAPI
struct SeasonItemView: View {
@EnvironmentObject var globalData: GlobalData
@EnvironmentObject var orientationInfo: OrientationInfo
@State private var isLoading: Bool = true
var item: ResumeItem
var item: BaseItemDto
@State private var episodes: [BaseItemDto] = []
init(item: ResumeItem) {
self.item = item
}
@State private var isLoading: Bool = true
@State private var viewDidLoad: Bool = false
func onAppear() {
let url = "/Users/\(globalData.user.user_id ?? "")/Items/\(item.Id)"
let request = RestRequest(method: .get, url: (globalData.server.baseURI ?? "") + url)
request.headerParameters["X-Emby-Authorization"] = globalData.authHeader
request.contentType = "application/json"
request.acceptType = "application/json"
request.responseData { (result: Result<RestResponse<Data>, RestError>) in
switch result {
case let .success(response):
let body = response.body
do {
let json = try JSON(data: body)
let responseItem = DetailItem()
responseItem.ProductionYear = json["ProductionYear"].int ?? 0
responseItem.Poster = json["ImageTags"]["Primary"].string ?? ""
responseItem.PosterBlurHash = json["ImageBlurHashes"]["Primary"][responseItem.Poster].string ?? ""
responseItem.Backdrop = json["BackdropImageTags"][0].string ?? ""
responseItem.BackdropBlurHash = json["ImageBlurHashes"]["Backdrop"][responseItem.Backdrop].string ?? ""
responseItem.Name = json["Name"].string ?? ""
responseItem.Type = json["Type"].string ?? ""
responseItem.IndexNumber = json["IndexNumber"].int ?? nil
responseItem.SeriesId = json["ParentId"].string ?? nil
responseItem.Id = item.Id
responseItem.Overview = json["Overview"].string ?? ""
responseItem.Tagline = json["Taglines"][0].string ?? ""
responseItem.SeriesName = json["SeriesName"].string ?? nil
responseItem.ParentId = json["ParentId"].string ?? ""
// People
responseItem.Directors = []
responseItem.Studios = []
responseItem.Writers = []
responseItem.Cast = []
responseItem.Genres = []
for (_, person): (String, JSON) in json["People"] {
if person["Type"].stringValue == "Director" {
responseItem.Directors.append(person["Name"].string ?? "")
} else if person["Type"].stringValue == "Writer" {
responseItem.Writers.append(person["Name"].string ?? "")
} else if person["Type"].stringValue == "Actor" {
let cast = CastMember()
cast.Name = person["Name"].string ?? ""
cast.Id = person["Id"].string ?? ""
let imageTag = person["PrimaryImageTag"].string ?? ""
cast.ImageBlurHash = person["ImageBlurHashes"]["Primary"][imageTag].string ?? ""
cast.Role = person["Role"].string ?? ""
cast
.Image =
URL(string: "\(globalData.server.baseURI ?? "")/Items/\(cast.Id)/Images/Primary?maxWidth=2000&quality=90&tag=\(imageTag)")!
responseItem.Cast.append(cast)
}
}
_fullItem.wrappedValue = responseItem
let url2 =
"/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)
request2.headerParameters["X-Emby-Authorization"] = globalData.authHeader
request2.contentType = "application/json"
request2.acceptType = "application/json"
request2.responseData { (result: Result<RestResponse<Data>, RestError>) in
switch result {
case let .success(response):
let body = response.body
do {
let jsonroot = try JSON(data: body)
for (_, json): (String, JSON) in jsonroot["Items"] {
let episode = DetailItem()
episode.ProductionYear = json["ProductionYear"].int ?? 0
episode.Poster = json["ImageTags"]["Primary"].string ?? ""
episode.PosterBlurHash = json["ImageBlurHashes"]["Primary"][fullItem.Poster].string ?? ""
episode.Backdrop = json["BackdropImageTags"][0].string ?? ""
episode.BackdropBlurHash = json["ImageBlurHashes"]["Backdrop"][fullItem.Backdrop].string ?? ""
episode.Name = json["Name"].string ?? ""
episode.Type = "Episode"
episode.IndexNumber = json["IndexNumber"].int ?? nil
episode.Id = json["Id"].string ?? ""
episode.ParentIndexNumber = json["ParentIndexNumber"].int ?? nil
episode.SeasonId = json["SeasonId"].string ?? nil
episode.SeriesId = json["SeriesId"].string ?? nil
episode.Overview = json["Overview"].string ?? ""
episode.SeriesName = json["SeriesName"].string ?? nil
episode.Progress = Double(json["UserData"]["PlaybackPositionTicks"].int ?? 0)
episode.OfficialRating = json["OfficialRating"].string ?? "PG-13"
episode.Watched = json["UserData"]["Played"].bool ?? false
episode.ParentId = episode.SeasonId ?? ""
episode.CommunityRating = String(json["CommunityRating"].float ?? 0.0)
var rI = ResumeItem()
rI.Name = episode.Name
rI.Id = episode.Id
rI.IndexNumber = episode.IndexNumber
rI.ParentIndexNumber = episode.ParentIndexNumber
rI.Image = episode.Poster
rI.ImageType = "Primary"
rI.BlurHash = episode.PosterBlurHash
rI.Type = "Episode"
rI.SeasonId = episode.SeasonId
rI.SeriesId = episode.SeriesId
rI.SeriesName = episode.SeriesName
rI.ProductionYear = episode.ProductionYear
episode.ResumeItem = rI
let seconds: Int = ((json["RunTimeTicks"].int ?? 0) / 10_000_000)
episode.RuntimeTicks = json["RunTimeTicks"].int ?? 0
let hours = (seconds / 3600)
let minutes = ((seconds - (hours * 3600)) / 60)
if hours != 0 {
episode.Runtime = "\(hours):\(String(minutes).leftPad(toWidth: 2, withString: "0"))"
} else {
episode.Runtime = "\(String(minutes).leftPad(toWidth: 2, withString: "0"))m"
}
if episode.Progress != 0 {
let remainingSecs = (Double(json["RunTimeTicks"].int ?? 0) - episode.Progress) / 10_000_000
let proghours = Int(remainingSecs / 3600)
let progminutes = Int((Int(remainingSecs) - (proghours * 3600)) / 60)
if proghours != 0 {
episode.ProgressStr = "\(proghours):\(String(progminutes).leftPad(toWidth: 2, withString: "0"))"
} else {
episode.ProgressStr = "\(String(progminutes).leftPad(toWidth: 2, withString: "0"))m"
}
}
_episodes.wrappedValue.append(episode)
}
_isLoading.wrappedValue = false
_hasAppearedOnce.wrappedValue = true
} catch {}
case let .failure(error):
debugPrint(error)
}
}
} catch {}
case let .failure(error):
debugPrint(error)
}
if(viewDidLoad) {
return
}
}
@Environment(\.verticalSizeClass)
var verticalSizeClass: UserInterfaceSizeClass?
@Environment(\.horizontalSizeClass)
var horizontalSizeClass: UserInterfaceSizeClass?
var isPortrait: Bool {
let result = verticalSizeClass == .regular && horizontalSizeClass == .compact
return result
TvShowsAPI.getEpisodes(seriesId: item.seriesId!, fields: [.primaryImageAspectRatio], seasonId: item.id!)
.sink(receiveCompletion: { completion in
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
isLoading = false
}, receiveValue: { response in
viewDidLoad = true
episodes = response.items ?? []
})
.store(in: &globalData.pendingAPIRequests)
}
@ViewBuilder
@ -181,11 +40,9 @@ struct SeasonItemView: View {
if isLoading {
EmptyView()
} else {
LazyImage(source: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(fullItem.SeriesId ?? "")/Images/Backdrop?maxWidth=550&quality=90&tag=\(item.SeasonImage ?? "")"))
LazyImage(source: item.getBackdropImage(baseURL: globalData.server.baseURI!, maxWidth: 1500))
.placeholderAndFailure {
Image(uiImage: UIImage(blurHash: item
.SeasonImageBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item
.SeasonImageBlurHash ?? "",
Image(uiImage: UIImage(blurHash: item.getBackdropImageBlurHash(),
size: CGSize(width: 32, height: 32))!)
.resizable()
}
@ -197,11 +54,9 @@ struct SeasonItemView: View {
var portraitHeaderOverlayView: some View {
HStack(alignment: .bottom, spacing: 12) {
LazyImage(source: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(fullItem.Id)/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)"))
LazyImage(source: item.getPrimaryImage(baseURL: globalData.server.baseURI!, maxWidth: 120))
.placeholderAndFailure {
Image(uiImage: UIImage(blurHash: fullItem
.PosterBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : fullItem
.PosterBlurHash,
Image(uiImage: UIImage(blurHash: item.getPrimaryImageBlurHash(),
size: CGSize(width: 32, height: 32))!)
.resizable()
.frame(width: 120, height: 180)
@ -211,13 +66,13 @@ struct SeasonItemView: View {
.frame(width: 120, height: 180)
.cornerRadius(10)
VStack(alignment: .leading) {
Text(fullItem.Name).font(.headline)
Text(item.name ?? "").font(.headline)
.fontWeight(.semibold)
.foregroundColor(.primary)
.fixedSize(horizontal: false, vertical: true)
.offset(y: -4)
if fullItem.ProductionYear != 0 {
Text(String(fullItem.ProductionYear)).font(.subheadline)
if item.productionYear != 0 {
Text(String(item.productionYear!)).font(.subheadline)
.fontWeight(.medium)
.foregroundColor(.secondary)
.lineLimit(1)
@ -235,23 +90,20 @@ struct SeasonItemView: View {
overlayAlignment: .bottomLeading,
headerHeight: UIScreen.main.bounds.width * 0.5625) {
LazyVStack(alignment: .leading) {
if fullItem.Tagline != "" {
Text(fullItem.Tagline).font(.body).italic().padding(.top, 7)
if !(item.taglines ?? []).isEmpty {
Text(item.taglines!.first!).font(.body).italic().padding(.top, 7)
.fixedSize(horizontal: false, vertical: true).padding(.leading, 16)
.padding(.trailing, 16)
}
Text(fullItem.Overview).font(.footnote).padding(.top, 3)
Text(item.overview ?? "").font(.footnote).padding(.top, 3)
.fixedSize(horizontal: false, vertical: true).padding(.bottom, 3).padding(.leading, 16)
.padding(.trailing, 16)
ForEach(episodes, id: \.Id) { episode in
NavigationLink(destination: ItemView(item: episode.ResumeItem ?? ResumeItem())) {
NavigationLink(destination: ItemView(item: episode)) {
HStack {
LazyImage(source: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(episode.Id)/Images/Primary?maxWidth=300&quality=90&tag=\(episode.Poster)"))
LazyImage(source: episode.getPrimaryImage(baseURL: globalData.server.baseURI!, maxWidth: 150))
.placeholderAndFailure {
Image(uiImage: UIImage(blurHash: episode
.PosterBlurHash == "" ?
"W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : fullItem
.PosterBlurHash,
Image(uiImage: UIImage(blurHash: episode.getPrimaryImageBlurHash()))
size: CGSize(width: 32, height: 32))!)
.resizable()
.frame(width: 150, height: 90)

View File

@ -6,87 +6,46 @@
*/
import SwiftUI
import SwiftyRequest
import SwiftyJSON
import NukeUI
import JellyfinAPI
struct SeriesItemView: View {
@EnvironmentObject var globalData: GlobalData
@EnvironmentObject private var globalData: GlobalData
@EnvironmentObject private var orientationInfo: OrientationInfo
var item: BaseItemDto;
@State private var seasons: [BaseItemDto] = [];
@State private var isLoading: Bool = true;
var item: ResumeItem;
@State private var items: [ResumeItem] = [];
@State private var hasAppearedOnce: Bool = false;
@State private var viewDidLoad: Bool = false;
func onAppear() {
recalcTracks()
if(hasAppearedOnce) {
if(viewDidLoad) {
return;
}
_isLoading.wrappedValue = true;
let url = "/Shows/\(item.Id )/Seasons?userId=\(globalData.user.user_id ?? "")&Fields=ItemCount"
isLoading = true
let request = RestRequest(method: .get, url: (globalData.server.baseURI ?? "") + url)
request.headerParameters["X-Emby-Authorization"] = globalData.authHeader
request.contentType = "application/json"
request.acceptType = "application/json"
request.responseData() { (result: Result<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()
itemObj.Type = "Season"
itemObj.Id = item["Id"].string ?? ""
itemObj.ProductionYear = item["ProductionYear"].int ?? 0
itemObj.ItemBadge = item["UserData"]["UnplayedItemCount"].int ?? 0
itemObj.Image = item["ImageTags"]["Primary"].string ?? ""
if(itemObj.Image == "") {
itemObj.Image = item["ParentBackdropImageTags"][0].string ?? ""
}
itemObj.ImageType = "Primary"
itemObj.SeasonImage = item["ParentBackdropImageTags"][0].string ?? ""
itemObj.SeasonImageType = "Backdrop"
itemObj.SeasonImageBlurHash = item["ImageBlurHashes"]["Backdrop"][itemObj.SeasonImage ?? ""].string ?? ""
itemObj.BlurHash = item["ImageBlurHashes"]["Primary"][itemObj.Image].string ?? ""
itemObj.SeriesName = item["SeriesName"].string ?? ""
itemObj.Name = item["Name"].string ?? ""
_items.wrappedValue.append(itemObj)
}
} catch {
}
break
case .failure(let error):
debugPrint(error)
break
}
_isLoading.wrappedValue = false;
_hasAppearedOnce.wrappedValue = true;
}
TvShowsAPI.getSeasons(seriesId: item.id ?? "")
.sink(receiveCompletion: { completion in
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
}, receiveValue: { response in
isLoading = false
viewDidLoad = true
seasons = response.items ?? []
})
.store(in: &globalData.pendingAPIRequests)
}
@Environment(\.verticalSizeClass) var verticalSizeClass: UserInterfaceSizeClass?
@Environment(\.horizontalSizeClass) var horizontalSizeClass: UserInterfaceSizeClass?
var isPortrait: Bool {
let result = verticalSizeClass == .regular && horizontalSizeClass == .compact
return result
}
//MARK: Grid tracks
func recalcTracks() {
let trkCnt: Int = Int(floor(UIScreen.main.bounds.size.width / 125));
_tracks.wrappedValue = []
tracks = []
for _ in (0..<trkCnt)
{
_tracks.wrappedValue.append(GridItem.init(.flexible()))
tracks.append(GridItem.init(.flexible()))
}
}
@State private var tracks: [GridItem] = []
var body: some View {
@ -94,43 +53,26 @@ struct SeriesItemView: View {
ScrollView(.vertical) {
Spacer().frame(height: 16)
LazyVGrid(columns: tracks) {
ForEach(items, id: \.Id) { item in
NavigationLink(destination: ItemView(item: item )) {
ForEach(seasons, id: \.id) { season in
NavigationLink(destination: ItemView(item: season )) {
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: season.getPrimaryImage(baseURL: globalData.server.baseURI!, maxWidth: 100))
.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: season.getPrimaryImageBlurHash(), size: CGSize(width: 32, height: 32))!)
.resizable()
.frame(width: 100, height: 150)
.cornerRadius(10)
}.overlay(
ZStack {
if(item.ItemBadge == 0) {
Image(systemName: "checkmark")
.font(.caption)
.padding(3)
.foregroundColor(.white)
} else {
Text("\(String(item.ItemBadge ?? 0))")
.font(.caption)
.padding(3)
.foregroundColor(.white)
}
}.background(Color.black)
.opacity(0.8)
.cornerRadius(10.0)
.padding(3), alignment: .topTrailing
)
}
.frame(width:100, height: 150)
.cornerRadius(10)
.shadow(radius: 5)
Text(item.Name)
Text(season.name ?? "")
.font(.caption)
.fontWeight(.semibold)
.foregroundColor(.primary)
.lineLimit(1)
if(item.ProductionYear != 0) {
Text(String(item.ProductionYear))
if(season.productionYear != 0) {
Text(String(season.productionYear!))
.foregroundColor(.secondary)
.font(.caption)
.fontWeight(.medium)
@ -139,13 +81,13 @@ struct SeriesItemView: View {
}
}
Spacer().frame(height: 2)
}.onChange(of: isPortrait) { ip in
}.onChange(of: orientationInfo.orientation) { ip in
recalcTracks()
}
}
}
.overrideViewPreference(.unspecified)
.onAppear(perform: onAppear)
.navigationTitle(item.Name)
.navigationTitle(item.name ?? "")
}
}

View File

@ -7,6 +7,20 @@
import Foundation
import Combine
import JellyfinAPI
struct LibraryFilters: Codable, Hashable {
var filters: [ItemFilter] = []
var sortOrder: [SortOrder] = [.descending]
var sortBy: [String] = ["SortName"]
}
public enum SortBy: String, Codable, CaseIterable {
case productionYear = "ProductionYear"
case premiereDate = "PremiereDate"
case name = "SortName"
case dateAdded = "DateCreated"
}
class justSignedIn: ObservableObject {
@Published var did: Bool = false