From 3b7778b3cfad37d3427381e90c7334bc495c7012 Mon Sep 17 00:00:00 2001 From: PangMo5 Date: Thu, 27 May 2021 21:29:30 +0900 Subject: [PATCH] ResumeItem class to struct LibraryView NavigationLink Recovery --- .../xcschemes/JellyfinPlayer.xcscheme | 78 ++ JellyfinPlayer/APIs/JellyfinAPI.swift | 24 +- JellyfinPlayer/ContentView.swift | 4 +- JellyfinPlayer/ContinueWatchingView.swift | 2 +- .../Library/ViewModels/LibraryViewModel.swift | 79 +- .../ViewModels/LibrarySearchViewModel.swift | 13 +- JellyfinPlayer/EpisodeItemView.swift | 660 ++++++++------- JellyfinPlayer/JellyApiTypings.swift | 38 +- JellyfinPlayer/LatestMediaView.swift | 2 +- JellyfinPlayer/LibraryFilterView.swift | 203 ++--- JellyfinPlayer/LibraryListView.swift | 25 +- JellyfinPlayer/LibrarySearchView.swift | 37 +- JellyfinPlayer/LibraryView.swift | 82 +- JellyfinPlayer/MovieItemView.swift | 764 +++++++++++------- JellyfinPlayer/NextUpView.swift | 2 +- JellyfinPlayer/SeasonItemView.swift | 2 +- JellyfinPlayer/SeriesItemView.swift | 2 +- JellyfinPlayer/Views/SettingsView.swift | 19 +- 18 files changed, 1148 insertions(+), 888 deletions(-) create mode 100644 JellyfinPlayer.xcodeproj/xcshareddata/xcschemes/JellyfinPlayer.xcscheme diff --git a/JellyfinPlayer.xcodeproj/xcshareddata/xcschemes/JellyfinPlayer.xcscheme b/JellyfinPlayer.xcodeproj/xcshareddata/xcschemes/JellyfinPlayer.xcscheme new file mode 100644 index 00000000..2cceb211 --- /dev/null +++ b/JellyfinPlayer.xcodeproj/xcshareddata/xcschemes/JellyfinPlayer.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/JellyfinPlayer/APIs/JellyfinAPI.swift b/JellyfinPlayer/APIs/JellyfinAPI.swift index b2941331..300038cd 100644 --- a/JellyfinPlayer/APIs/JellyfinAPI.swift +++ b/JellyfinPlayer/APIs/JellyfinAPI.swift @@ -26,8 +26,11 @@ enum ItemType: String { } enum SortType: String { - case name = "Name" + case name = "SortName" case dateCreated = "DateCreated" + case datePlayed = "DatePlayed" + case premiereDate = "PremiereDate" + case runtime = "Runtime" } enum ASC: String { @@ -37,20 +40,22 @@ enum ASC: String { enum FilterType: String { case isFavorite = "IsFavorite" + case isUnplayed = "IsUnplayed" } struct Filter { - var imageTypes = [ImageType]() - var fields = [Field]() - var itemTypes = [ItemType]() + var imageTypes: [ImageType] = [.primary, .backdrop, .thumb, .banner] + var fields: [Field] = [.primaryImageAspectRatio, .basicSyncInfo] + var itemTypes: [ItemType] = [.movie, .series] var filterTypes = [FilterType]() - var sort: SortType? - var asc: ASC? + var sort: SortType? = .dateCreated + var asc: ASC? = .descending var parentID: String? - var imageTypeLimit: Int? + var imageTypeLimit: Int? = 1 var recursive = true var genres = [String]() var personIds = [String]() + var officialRatings = [String]() } extension Filter { @@ -67,6 +72,7 @@ extension Filter { parameters["SortOrder"] = asc?.rawValue parameters["Genres"] = genres.joined(separator: ",") parameters["PersonIds"] = personIds.joined(separator: ",") + parameters["OfficialRatings"] = officialRatings.joined(separator: ",") return parameters } } @@ -125,7 +131,9 @@ extension JellyfinAPI: TargetType { case let .items(global, _, _), let .search(global, _, _, _): return [ - "X-Emby-Authorization": global.authHeader + "X-Emby-Authorization": global.authHeader, + "Content-Type": "application/json", + "Accept": "application/json" ] } } diff --git a/JellyfinPlayer/ContentView.swift b/JellyfinPlayer/ContentView.swift index a32d9dda..5a5d20f8 100644 --- a/JellyfinPlayer/ContentView.swift +++ b/JellyfinPlayer/ContentView.swift @@ -197,9 +197,9 @@ struct ContentView: View { HStack() { Text("Latest \(library_names[library_id] ?? "")").font(.title2).fontWeight(.bold).padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 16)) Spacer() -// NavigationLink(destination: LibraryView(prefill: library_id, names: [library_id: library_names[library_id] ?? ""], libraries: [library_id], filter: "&SortBy=DateCreated&SortOrder=Descending")) { + NavigationLink(destination: LibraryView(viewModel: .init(filter: Filter(parentID: library_id)), title: library_names[library_id] ?? "")) { Text("See All").font(.subheadline).fontWeight(.bold) -// } + } }.padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16)) LatestMediaView(library: library_id) }.padding(EdgeInsets(top: 4, leading: 0, bottom: 0, trailing: 0)) diff --git a/JellyfinPlayer/ContinueWatchingView.swift b/JellyfinPlayer/ContinueWatchingView.swift index 569ee357..0ad87ff1 100644 --- a/JellyfinPlayer/ContinueWatchingView.swift +++ b/JellyfinPlayer/ContinueWatchingView.swift @@ -64,7 +64,7 @@ struct ContinueWatchingView: View { let json = try JSON(data: body) for (_,item):(String, JSON) in json["Items"] { // Do something you want - let itemObj = ResumeItem() + var itemObj = ResumeItem() if(item["PrimaryImageAspectRatio"].double ?? 0.0 < 1.0) { //portrait; use backdrop instead itemObj.Image = item["BackdropImageTags"][0].string ?? "" diff --git a/JellyfinPlayer/Domains/Library/ViewModels/LibraryViewModel.swift b/JellyfinPlayer/Domains/Library/ViewModels/LibraryViewModel.swift index 0dae2938..70a970e5 100644 --- a/JellyfinPlayer/Domains/Library/ViewModels/LibraryViewModel.swift +++ b/JellyfinPlayer/Domains/Library/ViewModels/LibraryViewModel.swift @@ -12,9 +12,9 @@ import Moya import SwiftyJSON final class LibraryViewModel: ObservableObject { - fileprivate var provider = MoyaProvider(plugins: [NetworkLoggerPlugin(configuration: NetworkLoggerPlugin.Configuration(logOptions: .verbose))]) + fileprivate var provider = + MoyaProvider(plugins: [NetworkLoggerPlugin()]) - var prefillID: String @Published var filter: Filter @@ -31,64 +31,59 @@ final class LibraryViewModel: ObservableObject { var page = 1 - var globalData = GlobalData() + var globalData = GlobalData() { + didSet { + injectEnvironmentData() + } + } fileprivate var cancellables = Set() - init(prefillID: String, - filter: Filter? = nil) - { - self.prefillID = prefillID + init(filter: Filter = Filter()) { + self.filter = filter + } - if let unwrappedFilter = filter { - self.filter = unwrappedFilter - } else { - self.filter = Filter(imageTypes: [.primary, .backdrop, .thumb, .banner], - fields: [.primaryImageAspectRatio, .basicSyncInfo], - itemTypes: [.movie, .series], - sort: .dateCreated, - asc: .descending, - parentID: prefillID, - imageTypeLimit: 1, - recursive: true) - } + fileprivate func injectEnvironmentData() { + cancellables.removeAll() + + $filter + .sink(receiveValue: requestInitItems(_:)) + .store(in: &cancellables) } func requestNextPage() { page += 1 - requestItems() + requestItems(filter) } func requestPreviousPage() { page -= 1 - requestItems() - } - - func requestInitItems() { - page = 1 - requestItems() + requestItems(filter) } - fileprivate func requestItems() { - print(globalData.server?.baseURI) + func requestInitItems(_ filter: Filter) { + page = 1 + requestItems(filter) + } + + fileprivate func requestItems(_ filter: Filter) { + print("ASDASDA") print(globalData.authHeader) - print(filter) isLoading = true provider.requestPublisher(.items(globalData: globalData, filter: filter, page: page)) // .map(ResumeItem.self) TO DO .print() - .sink(receiveCompletion: { _ in - self.isLoading = false - }, receiveValue: { response in - self.items.removeAll() + .receive(on: DispatchQueue.main) + .map { response -> ([ResumeItem], Int) in let body = response.data var totalCount = 0 + var innerItems = [ResumeItem]() do { let json = try JSON(data: body) totalCount = json["TotalRecordCount"].int ?? 0 for (_, item): (String, JSON) in json["Items"] { // Do something you want - let itemObj = ResumeItem() + var itemObj = ResumeItem() itemObj.Type = item["Type"].string ?? "" if itemObj.Type == "Series" { itemObj.ItemBadge = item["UserData"]["UnplayedItemCount"].int ?? 0 @@ -120,21 +115,29 @@ final class LibraryViewModel: ObservableObject { } itemObj.Watched = item["UserData"]["Played"].bool ?? false - self.items.append(itemObj) + innerItems.append(itemObj) } } catch {} - - if totalCount > 100 { + return (innerItems, totalCount) + } + .sink(receiveCompletion: { [weak self] _ in + guard let self = self else { return } + self.isLoading = false + }, receiveValue: { [weak self] items, count in + guard let self = self else { return } + if count > 100 { if self.page > 1 { self.isHiddenPreviousButton = false } - if totalCount > (self.page * 100) { + if count > (self.page * 100) { self.isHiddenNextButton = false } } else { self.isHiddenNextButton = true self.isHiddenPreviousButton = true } + + self.items = items }) .store(in: &cancellables) } diff --git a/JellyfinPlayer/Domains/Search/ViewModels/LibrarySearchViewModel.swift b/JellyfinPlayer/Domains/Search/ViewModels/LibrarySearchViewModel.swift index 92d28ca5..c1ee031b 100644 --- a/JellyfinPlayer/Domains/Search/ViewModels/LibrarySearchViewModel.swift +++ b/JellyfinPlayer/Domains/Search/ViewModels/LibrarySearchViewModel.swift @@ -52,16 +52,18 @@ final class LibrarySearchViewModel: ObservableObject { provider.requestPublisher(.search(globalData: globalData, filter: filter, searchQuery: query, page: page)) // .map(ResumeItem.self) TO DO .print() - .sink(receiveCompletion: { _ in + .sink(receiveCompletion: { [weak self] _ in + guard let self = self else { return } self.isLoading = false - }, receiveValue: { response in + }, receiveValue: { [weak self] response in + guard let self = self else { return } let body = response.data - self.items.removeAll() + var innerItems = [ResumeItem]() do { let json = try JSON(data: body) for (_, item): (String, JSON) in json["Items"] { // Do something you want - let itemObj = ResumeItem() + var itemObj = ResumeItem() itemObj.Type = item["Type"].string ?? "" if itemObj.Type == "Series" { itemObj.ItemBadge = item["UserData"]["UnplayedItemCount"].int ?? 0 @@ -93,9 +95,10 @@ final class LibrarySearchViewModel: ObservableObject { } itemObj.Watched = item["UserData"]["Played"].bool ?? false - self.items.append(itemObj) + innerItems.append(itemObj) } } catch {} + self.items = innerItems }) .store(in: &cancellables) } diff --git a/JellyfinPlayer/EpisodeItemView.swift b/JellyfinPlayer/EpisodeItemView.swift index 5f293bf3..23d2a387 100644 --- a/JellyfinPlayer/EpisodeItemView.swift +++ b/JellyfinPlayer/EpisodeItemView.swift @@ -5,91 +5,107 @@ // Created by Aiden Vigue on 5/13/21. // -import SwiftUI -import SwiftyRequest -import SwiftyJSON import SDWebImageSwiftUI +import SwiftUI +import SwiftyJSON +import SwiftyRequest struct EpisodeItemView: View { - @EnvironmentObject private var globalData: GlobalData - @EnvironmentObject private var orientationInfo: OrientationInfo - @EnvironmentObject private var playbackInfo: ItemPlayback - var item: ResumeItem; - var fullItem: DetailItem; + @EnvironmentObject + private var globalData: GlobalData + @EnvironmentObject + private var orientationInfo: OrientationInfo + @EnvironmentObject + private var playbackInfo: ItemPlayback + var item: ResumeItem + var fullItem: DetailItem - @State private var isLoading: Bool = true; - @State private var progressString: String = ""; - @State private var viewDidLoad: Bool = false; - - @State private var watched: Bool = false { + @State + private var isLoading: Bool = true + @State + private var progressString: String = "" + @State + private var viewDidLoad: Bool = false + + @State + private var watched: Bool = false { didSet { - if(watched == true) { + if watched == true { let date = Date() let formatter = DateFormatter() formatter.locale = Locale(identifier: "en_US_POSIX") formatter.timeZone = TimeZone(secondsFromGMT: 0) formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ" - let request = RestRequest(method: .post, url: (globalData.server?.baseURI ?? "") + "/Users/\(globalData.user?.user_id ?? "")/PlayedItems/\(fullItem.Id)?DatePlayed=\(formatter.string(from: date).replacingOccurrences(of: ":", with: "%3A"))") + let request = RestRequest(method: .post, + url: (globalData.server?.baseURI ?? "") + + "/Users/\(globalData.user?.user_id ?? "")/PlayedItems/\(fullItem.Id)?DatePlayed=\(formatter.string(from: date).replacingOccurrences(of: ":", with: "%3A"))") request.headerParameters["X-Emby-Authorization"] = globalData.authHeader request.contentType = "application/json" request.acceptType = "application/json" - - request.responseData() { (result: Result, RestError>) in + + request.responseData { (_: Result, RestError>) in } } else { - let request = RestRequest(method: .delete, url: (globalData.server?.baseURI ?? "") + "/Users/\(globalData.user?.user_id ?? "")/PlayedItems/\(fullItem.Id)") + let request = RestRequest(method: .delete, + url: (globalData.server?.baseURI ?? "") + + "/Users/\(globalData.user?.user_id ?? "")/PlayedItems/\(fullItem.Id)") request.headerParameters["X-Emby-Authorization"] = globalData.authHeader request.contentType = "application/json" request.acceptType = "application/json" - - request.responseData() { (result: Result, RestError>) in + + request.responseData { (_: Result, RestError>) in } } } - }; - - @State private var favorite: Bool = false { - didSet { - if(favorite == true) { - let request = RestRequest(method: .post, url: (globalData.server?.baseURI ?? "") + "/Users/\(globalData.user?.user_id ?? "")/FavoriteItems/\(fullItem.Id)") - request.headerParameters["X-Emby-Authorization"] = globalData.authHeader - request.contentType = "application/json" - request.acceptType = "application/json" - - request.responseData() { (result: Result, RestError>) in - } - } else { - let request = RestRequest(method: .delete, url: (globalData.server?.baseURI ?? "") + "/Users/\(globalData.user?.user_id ?? "")/FavoriteItems/\(fullItem.Id)") - request.headerParameters["X-Emby-Authorization"] = globalData.authHeader - request.contentType = "application/json" - request.acceptType = "application/json" - - request.responseData() { (result: Result, RestError>) in - } - } - } - }; - - init(item: ResumeItem) { - self.item = item; - fullItem = DetailItem(); } - + + @State + private var favorite: Bool = false { + didSet { + if favorite == true { + let request = RestRequest(method: .post, + url: (globalData.server?.baseURI ?? "") + + "/Users/\(globalData.user?.user_id ?? "")/FavoriteItems/\(fullItem.Id)") + request.headerParameters["X-Emby-Authorization"] = globalData.authHeader + request.contentType = "application/json" + request.acceptType = "application/json" + + request.responseData { (_: Result, RestError>) in + } + } else { + let request = RestRequest(method: .delete, + url: (globalData.server?.baseURI ?? "") + + "/Users/\(globalData.user?.user_id ?? "")/FavoriteItems/\(fullItem.Id)") + request.headerParameters["X-Emby-Authorization"] = globalData.authHeader + request.contentType = "application/json" + request.acceptType = "application/json" + + request.responseData { (_: Result, RestError>) in + } + } + } + } + + init(item: ResumeItem) { + self.item = item + self.fullItem = DetailItem() + } + func loadData() { - if(_viewDidLoad.wrappedValue == true) { + if _viewDidLoad.wrappedValue == true { return } - _viewDidLoad.wrappedValue = true; + _viewDidLoad.wrappedValue = true let url = "/Users/\(globalData.user?.user_id ?? "")/Items/\(item.Id)" - + let request = RestRequest(method: .get, url: (globalData.server?.baseURI ?? "") + url) request.headerParameters["X-Emby-Authorization"] = globalData.authHeader request.contentType = "application/json" request.acceptType = "application/json" - - request.responseData() { (result: Result, RestError>) in + + request.responseData { (result: Result, RestError>) in switch result { - case .success(let response): + case let .success(response): let body = response.body do { let json = try JSON(data: body) @@ -110,225 +126,256 @@ struct EpisodeItemView: View { fullItem.SeriesName = json["SeriesName"].string ?? nil fullItem.Progress = Double(json["UserData"]["PlaybackPositionTicks"].int ?? 0) fullItem.OfficialRating = json["OfficialRating"].string ?? "PG-13" - fullItem.Watched = json["UserData"]["Played"].bool ?? false; - fullItem.CommunityRating = String(json["CommunityRating"].float ?? 0.0); - fullItem.CriticRating = String(json["CriticRating"].int ?? 0); + fullItem.Watched = json["UserData"]["Played"].bool ?? false + fullItem.CommunityRating = String(json["CommunityRating"].float ?? 0.0) + fullItem.CriticRating = String(json["CriticRating"].int ?? 0) fullItem.ParentId = json["ParentId"].string ?? "" fullItem.ParentBackdropItemId = json["ParentBackdropItemId"].string ?? "" - //People + // People fullItem.Directors = [] fullItem.Studios = [] fullItem.Writers = [] fullItem.Cast = [] fullItem.Genres = [] - - for (_,person):(String, JSON) in json["People"] { - if(person["Type"].stringValue == "Director") { - fullItem.Directors.append(person["Name"].string ?? ""); - } else if(person["Type"].stringValue == "Writer") { - fullItem.Writers.append(person["Name"].string ?? ""); - } else if(person["Type"].stringValue == "Actor") { - let cast = CastMember(); - cast.Name = person["Name"].string ?? ""; - cast.Id = person["Id"].string ?? ""; - let imageTag = person["PrimaryImageTag"].string ?? ""; - cast.ImageBlurHash = person["ImageBlurHashes"]["Primary"][imageTag].string ?? ""; - cast.Role = person["Role"].string ?? ""; - cast.Image = URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(cast.Id)/Images/Primary?maxHeight=250&quality=85&tag=\(imageTag)")! - fullItem.Cast.append(cast); + + for (_, person): (String, JSON) in json["People"] { + if person["Type"].stringValue == "Director" { + fullItem.Directors.append(person["Name"].string ?? "") + } else if person["Type"].stringValue == "Writer" { + fullItem.Writers.append(person["Name"].string ?? "") + } else if person["Type"].stringValue == "Actor" { + let cast = CastMember() + cast.Name = person["Name"].string ?? "" + cast.Id = person["Id"].string ?? "" + let imageTag = person["PrimaryImageTag"].string ?? "" + cast.ImageBlurHash = person["ImageBlurHashes"]["Primary"][imageTag].string ?? "" + cast.Role = person["Role"].string ?? "" + cast + .Image = + URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(cast.Id)/Images/Primary?maxHeight=250&quality=85&tag=\(imageTag)")! + fullItem.Cast.append(cast) } } - - //Studios - for (_,studio):(String, JSON) in json["Studios"] { - fullItem.Studios.append(studio["Name"].string ?? ""); + + // Studios + for (_, studio): (String, JSON) in json["Studios"] { + fullItem.Studios.append(studio["Name"].string ?? "") } - - //Genres - for (_,genre):(String, JSON) in json["GenreItems"] { + + // Genres + for (_, genre): (String, JSON) in json["GenreItems"] { let tmpGenre = IVGenre() - tmpGenre.Id = genre["Id"].string ?? ""; - tmpGenre.Name = genre["Name"].string ?? ""; - fullItem.Genres.append(tmpGenre); + tmpGenre.Id = genre["Id"].string ?? "" + tmpGenre.Name = genre["Name"].string ?? "" + fullItem.Genres.append(tmpGenre) } - + _watched.wrappedValue = fullItem.Watched - _favorite.wrappedValue = json["UserData"]["IsFavorite"].bool ?? false; - - //Process runtime - let seconds: Int = ((json["RunTimeTicks"].int ?? 0)/10000000) - fullItem.RuntimeTicks = json["RunTimeTicks"].int ?? 0; - let hours = (seconds/3600) - let minutes = ((seconds - (hours * 3600))/60) - if(hours != 0) { + _favorite.wrappedValue = json["UserData"]["IsFavorite"].bool ?? false + + // Process runtime + let seconds: Int = ((json["RunTimeTicks"].int ?? 0) / 10_000_000) + fullItem.RuntimeTicks = json["RunTimeTicks"].int ?? 0 + let hours = (seconds / 3600) + let minutes = ((seconds - (hours * 3600)) / 60) + if hours != 0 { fullItem.Runtime = "\(hours):\(String(minutes).leftPad(toWidth: 2, withString: "0"))" } else { fullItem.Runtime = "\(String(minutes).leftPad(toWidth: 2, withString: "0"))m" } - - if(fullItem.Progress != 0) { - let remainingSecs = (Double(json["RunTimeTicks"].int ?? 0) - fullItem.Progress)/10000000 - let proghours = Int(remainingSecs/3600) - let progminutes = Int((Int(remainingSecs) - (proghours * 3600))/60) - if(proghours != 0) { + + if fullItem.Progress != 0 { + let remainingSecs = (Double(json["RunTimeTicks"].int ?? 0) - fullItem.Progress) / 10_000_000 + let proghours = Int(remainingSecs / 3600) + let progminutes = Int((Int(remainingSecs) - (proghours * 3600)) / 60) + if proghours != 0 { _progressString.wrappedValue = "\(proghours):\(String(progminutes).leftPad(toWidth: 2, withString: "0"))" } else { _progressString.wrappedValue = "\(String(progminutes).leftPad(toWidth: 2, withString: "0"))m" } } - } catch { - - } - break - case .failure(let error): + } catch {} + case let .failure(error): debugPrint(error) - break } - _isLoading.wrappedValue = false; + _isLoading.wrappedValue = false } } - + var body: some View { LoadingView(isShowing: $isLoading) { - VStack(alignment:.leading) { - if(!isLoading) { - if(orientationInfo.orientation == .portrait) { + VStack(alignment: .leading) { + if !isLoading { + if orientationInfo.orientation == .portrait { GeometryReader { geometry in - VStack() { + VStack { WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.ParentBackdropItemId)/Images/Backdrop?maxWidth=550&quality=90&tag=\(fullItem.Backdrop)")!) .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size .placeholder { - Image(uiImage: UIImage(blurHash: (fullItem.BackdropBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : fullItem.BackdropBlurHash), size: CGSize(width: 32, height: 32))!) + Image(uiImage: UIImage(blurHash: fullItem + .BackdropBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : fullItem + .BackdropBlurHash, + size: CGSize(width: 32, height: 32))!) .resizable() - .frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing, height: UIDevice.current.userInterfaceIdiom == .pad ? 350 : (geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing) * 0.5625) + .frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets + .trailing, + height: UIDevice.current + .userInterfaceIdiom == .pad ? 350 : + (geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets + .trailing) * 0.5625) } - + .opacity(0.3) .aspectRatio(contentMode: .fill) - .frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing, height: UIDevice.current.userInterfaceIdiom == .pad ? 350 : (geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing) * 0.5625) + .frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing, + height: UIDevice.current + .userInterfaceIdiom == .pad ? 350 : + (geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing) * + 0.5625) .shadow(radius: 5) - .overlay( - HStack() { - WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.SeriesId ?? "")/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)")!) - .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size - .placeholder { - Image(uiImage: UIImage(blurHash: (fullItem.PosterBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : fullItem.PosterBlurHash), size: CGSize(width: 32, height: 32))!) - .resizable() - .frame(width: 120, height: 180) - .cornerRadius(10) - }.aspectRatio(contentMode: .fill) - .frame(width: 120, height: 180) - .cornerRadius(10) - VStack(alignment: .leading) { - Spacer() - Text(fullItem.Name).font(.headline) - .fontWeight(.semibold) - .foregroundColor(.primary) - .fixedSize(horizontal: false, vertical: true) - .offset(y: -4) - HStack() { - Text(String(fullItem.ProductionYear)).font(.subheadline) - .fontWeight(.medium) + .overlay(HStack { + WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.SeriesId ?? "")/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)")!) + .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size + .placeholder { + Image(uiImage: UIImage(blurHash: fullItem + .PosterBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : + fullItem.PosterBlurHash, + size: CGSize(width: 32, height: 32))!) + .resizable() + .frame(width: 120, height: 180) + .cornerRadius(10) + }.aspectRatio(contentMode: .fill) + .frame(width: 120, height: 180) + .cornerRadius(10) + VStack(alignment: .leading) { + Spacer() + Text(fullItem.Name).font(.headline) + .fontWeight(.semibold) + .foregroundColor(.primary) + .fixedSize(horizontal: false, vertical: true) + .offset(y: -4) + HStack { + Text(String(fullItem.ProductionYear)).font(.subheadline) + .fontWeight(.medium) + .foregroundColor(.secondary) + .lineLimit(1) + Text(fullItem.Runtime).font(.subheadline) + .fontWeight(.medium) + .foregroundColor(.secondary) + .lineLimit(1) + if fullItem.OfficialRating != "" { + Text(fullItem.OfficialRating).font(.subheadline) + .fontWeight(.semibold) .foregroundColor(.secondary) .lineLimit(1) - Text(fullItem.Runtime).font(.subheadline) - .fontWeight(.medium) - .foregroundColor(.secondary) - .lineLimit(1) - if(fullItem.OfficialRating != "") { - Text(fullItem.OfficialRating).font(.subheadline) + .padding(EdgeInsets(top: 1, leading: 4, bottom: 1, trailing: 4)) + .overlay(RoundedRectangle(cornerRadius: 2) + .stroke(Color.secondary, lineWidth: 1)) + } + if fullItem.CommunityRating != "0" { + HStack { + Image(systemName: "star").foregroundColor(.secondary) + Text(fullItem.CommunityRating).font(.subheadline) .fontWeight(.semibold) .foregroundColor(.secondary) .lineLimit(1) - .padding(EdgeInsets(top: 1, leading: 4, bottom: 1, trailing: 4)) - .overlay( - RoundedRectangle(cornerRadius: 2) - .stroke(Color.secondary, lineWidth: 1) - ) + .offset(x: -7, y: 0.7) } - if(fullItem.CommunityRating != "0") { - HStack() { - Image(systemName: "star").foregroundColor(.secondary) - Text(fullItem.CommunityRating).font(.subheadline) - .fontWeight(.semibold) - .foregroundColor(.secondary) - .lineLimit(1) - .offset(x: -7, y: 0.7) - } - } - }.frame(maxWidth: .infinity, alignment: .leading) - }.frame(maxWidth: .infinity, alignment: .leading).offset(x: 0, y: UIDevice.current.userInterfaceIdiom == .pad ? -98 : -46).padding(.trailing, 16) - }.offset(x: 16, y: UIDevice.current.userInterfaceIdiom == .pad ? 135 : 40) - , alignment: .bottomLeading) + } + }.frame(maxWidth: .infinity, alignment: .leading) + }.frame(maxWidth: .infinity, alignment: .leading) + .offset(x: 0, y: UIDevice.current.userInterfaceIdiom == .pad ? -98 : -46) + .padding(.trailing, 16) + }.offset(x: 16, y: UIDevice.current.userInterfaceIdiom == .pad ? 135 : 40), + alignment: .bottomLeading) VStack(alignment: .leading) { - HStack() { - //Play button + HStack { + // Play button Button { - self.playbackInfo.itemToPlay = fullItem; - self.playbackInfo.shouldPlay = true; + self.playbackInfo.itemToPlay = fullItem + self.playbackInfo.shouldPlay = true } label: { - HStack() { - Text(fullItem.Progress == 0 ? "Play" : "\(progressString) left").foregroundColor(Color.white).font(.callout).fontWeight(.semibold) + HStack { + Text(fullItem.Progress == 0 ? "Play" : "\(progressString) left") + .foregroundColor(Color.white).font(.callout).fontWeight(.semibold) Image(systemName: "play.fill").foregroundColor(Color.white).font(.system(size: 20)) } .frame(width: 120, height: 35) - .background(Color(red: 172/255, green: 92/255, blue: 195/255)) + .background(Color(red: 172 / 255, green: 92 / 255, blue: 195 / 255)) .cornerRadius(10) }.buttonStyle(PlainButtonStyle()) - .frame(width: 120, height: 35) + .frame(width: 120, height: 35) Spacer() - HStack() { - Button() { + HStack { + Button { favorite.toggle() } label: { - if(!favorite) { + if !favorite { Image(systemName: "heart").foregroundColor(Color.primary).font(.system(size: 20)) } else { - Image(systemName: "heart.fill").foregroundColor(Color(UIColor.systemRed)).font(.system(size: 20)) + Image(systemName: "heart.fill").foregroundColor(Color(UIColor.systemRed)) + .font(.system(size: 20)) } } - Button() { + Button { watched.toggle() } label: { - if(watched) { - Image(systemName: "checkmark.rectangle.fill").foregroundColor(Color.primary).font(.system(size: 20)) + if watched { + Image(systemName: "checkmark.rectangle.fill").foregroundColor(Color.primary) + .font(.system(size: 20)) } else { - Image(systemName: "xmark.rectangle").foregroundColor(Color.primary).font(.system(size: 20)) + Image(systemName: "xmark.rectangle").foregroundColor(Color.primary) + .font(.system(size: 20)) } } } - }.padding(.leading, 16).padding(.trailing,16) - ScrollView() { + }.padding(.leading, 16).padding(.trailing, 16) + ScrollView { VStack(alignment: .leading) { - if(fullItem.Tagline != "") { - Text(fullItem.Tagline).font(.body).italic().padding(.top, 7).fixedSize(horizontal: false, vertical: true).padding(.leading, 16).padding(.trailing,16) + if fullItem.Tagline != "" { + Text(fullItem.Tagline).font(.body).italic().padding(.top, 7) + .fixedSize(horizontal: false, vertical: true).padding(.leading, 16) + .padding(.trailing, 16) } - Text(fullItem.Overview).font(.footnote).padding(.top, 3).fixedSize(horizontal: false, vertical: true).padding(.bottom, 3).padding(.leading, 16).padding(.trailing,16) - if(fullItem.Genres.count != 0) { + Text(fullItem.Overview).font(.footnote).padding(.top, 3) + .fixedSize(horizontal: false, vertical: true).padding(.bottom, 3).padding(.leading, 16) + .padding(.trailing, 16) + if !fullItem.Genres.isEmpty { ScrollView(.horizontal, showsIndicators: false) { - HStack() { + HStack { Text("Genres:").font(.callout).fontWeight(.semibold) - ForEach(fullItem.Genres, id: \.Id) {genre in -// NavigationLink(destination: LibraryView(extraParams: "&Genres=\(genre.Name.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "")", title: genre.Name)) { + ForEach(fullItem.Genres, id: \.Id) { genre in + NavigationLink(destination: LibraryView(viewModel: .init(filter: Filter(genres: [ + genre + .Name, + ])), title: genre.Name)) { Text(genre.Name).font(.footnote) -// } + } } - }.padding(.leading, 16).padding(.trailing,16) + }.padding(.leading, 16).padding(.trailing, 16) } } - if(fullItem.Cast.count != 0) { + if !fullItem.Cast.isEmpty { ScrollView(.horizontal, showsIndicators: false) { - VStack() { - Spacer().frame(height: 8); - HStack() { + VStack { + Spacer().frame(height: 8) + HStack { Spacer().frame(width: 16) ForEach(fullItem.Cast, id: \.Id) { cast in -// NavigationLink(destination: LibraryView(extraParams: "&PersonIds=\(cast.Id)", title: cast.Name)) { - VStack() { + NavigationLink(destination: LibraryView(viewModel: .init(filter: Filter(personIds: [ + cast + .Id, + ])), title: cast.Name)) { + VStack { WebImage(url: cast.Image) .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size .placeholder { - Image(uiImage: UIImage(blurHash: (cast.ImageBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : cast.ImageBlurHash), size: CGSize(width: 16, height: 16))!) + Image(uiImage: UIImage(blurHash: cast + .ImageBlurHash == "" ? + "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : + cast.ImageBlurHash, + size: CGSize(width: 16, + height: 16))!) .resizable() .aspectRatio(contentMode: .fill) .frame(width: 100, height: 100) @@ -337,12 +384,14 @@ struct EpisodeItemView: View { .aspectRatio(contentMode: .fill) .frame(width: 100, height: 100) .cornerRadius(10) - Text(cast.Name).font(.footnote).fontWeight(.regular).lineLimit(1).frame(width: 100).foregroundColor(Color.primary) - if(cast.Role != "") { - Text(cast.Role).font(.caption).fontWeight(.medium).lineLimit(1).foregroundColor(Color.secondary).frame(width: 100) + Text(cast.Name).font(.footnote).fontWeight(.regular).lineLimit(1) + .frame(width: 100).foregroundColor(Color.primary) + if cast.Role != "" { + Text(cast.Role).font(.caption).fontWeight(.medium).lineLimit(1) + .foregroundColor(Color.secondary).frame(width: 100) } } -// } + } Spacer().frame(width: 10) } Spacer().frame(width: 16) @@ -350,51 +399,66 @@ struct EpisodeItemView: View { } }.padding(.top, -3) } - if(fullItem.Directors.count != 0) { - HStack() { + if !fullItem.Directors.isEmpty { + HStack { Text("Directors:").font(.callout).fontWeight(.semibold) - Text(fullItem.Directors.joined(separator: ", ")).font(.footnote).lineLimit(1).foregroundColor(Color.secondary) - }.padding(.leading, 16).padding(.trailing,16) + Text(fullItem.Directors.joined(separator: ", ")).font(.footnote).lineLimit(1) + .foregroundColor(Color.secondary) + }.padding(.leading, 16).padding(.trailing, 16) } - if(fullItem.Writers.count != 0) { - HStack() { + if !fullItem.Writers.isEmpty { + HStack { Text("Writers:").font(.callout).fontWeight(.semibold) - Text(fullItem.Writers.joined(separator: ", ")).font(.footnote).lineLimit(1).foregroundColor(Color.secondary) - }.padding(.leading, 16).padding(.trailing,16) + Text(fullItem.Writers.joined(separator: ", ")).font(.footnote).lineLimit(1) + .foregroundColor(Color.secondary) + }.padding(.leading, 16).padding(.trailing, 16) } - if(fullItem.Studios.count != 0) { - HStack() { + if !fullItem.Studios.isEmpty { + HStack { Text("Studios:").font(.callout).fontWeight(.semibold) - Text(fullItem.Studios.joined(separator: ", ")).font(.footnote).lineLimit(1).foregroundColor(Color.secondary) - }.padding(.leading, 16).padding(.trailing,16) + Text(fullItem.Studios.joined(separator: ", ")).font(.footnote).lineLimit(1) + .foregroundColor(Color.secondary) + }.padding(.leading, 16).padding(.trailing, 16) } Spacer().frame(height: 3) } } - }.padding(EdgeInsets(top: UIDevice.current.userInterfaceIdiom == .pad ? 54 : 24, leading: 0, bottom: 0, trailing: 0)) + } + .padding(EdgeInsets(top: UIDevice.current.userInterfaceIdiom == .pad ? 54 : 24, leading: 0, bottom: 0, + trailing: 0)) } } } else { GeometryReader { geometry in - ZStack() { + ZStack { WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.ParentBackdropItemId)/Images/Backdrop?maxWidth=750&quality=90&tag=\(fullItem.Backdrop)")!) .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size .placeholder { - Image(uiImage: UIImage(blurHash: (fullItem.BackdropBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : fullItem.BackdropBlurHash), size: CGSize(width: 32, height: 32))!) + Image(uiImage: UIImage(blurHash: fullItem + .BackdropBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : fullItem + .BackdropBlurHash, + size: CGSize(width: 32, height: 32))!) .resizable() - .frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing, height: geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom) + .frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets + .trailing, + height: geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets + .bottom) } - + .opacity(0.3) .aspectRatio(contentMode: .fill) - .frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing, height: geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom) + .frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing, + height: geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom) .edgesIgnoringSafeArea(.all) - HStack() { - VStack() { + HStack { + VStack { WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.SeriesId ?? "")/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)")!) .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size .placeholder { - Image(uiImage: UIImage(blurHash: (fullItem.PosterBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : fullItem.PosterBlurHash), size: CGSize(width: 32, height: 32))!) + Image(uiImage: UIImage(blurHash: fullItem + .PosterBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : + fullItem.PosterBlurHash, + size: CGSize(width: 32, height: 32))!) .resizable() .frame(width: 120, height: 180) } @@ -404,23 +468,24 @@ struct EpisodeItemView: View { .shadow(radius: 5) Spacer().frame(height: 15) Button { - self.playbackInfo.itemToPlay = fullItem; - self.playbackInfo.shouldPlay = true; + self.playbackInfo.itemToPlay = fullItem + self.playbackInfo.shouldPlay = true } label: { - HStack() { - Text(fullItem.Progress == 0 ? "Play" : "\(progressString) left").foregroundColor(Color.white).font(.callout).fontWeight(.semibold) + HStack { + Text(fullItem.Progress == 0 ? "Play" : "\(progressString) left") + .foregroundColor(Color.white).font(.callout).fontWeight(.semibold) Image(systemName: "play.fill").foregroundColor(Color.white).font(.system(size: 20)) } .frame(width: 120, height: 35) - .background(Color(red: 172/255, green: 92/255, blue: 195/255)) + .background(Color(red: 172 / 255, green: 92 / 255, blue: 195 / 255)) .cornerRadius(10) }.buttonStyle(PlainButtonStyle()) - .frame(width: 120, height: 35) + .frame(width: 120, height: 35) Spacer() } - ScrollView() { + ScrollView { VStack(alignment: .leading) { - HStack() { + HStack { VStack(alignment: .leading) { Text(fullItem.Name).font(.headline) .fontWeight(.semibold) @@ -428,7 +493,7 @@ struct EpisodeItemView: View { .fixedSize(horizontal: false, vertical: true) .offset(x: 14, y: 0) Spacer().frame(height: 1) - HStack() { + HStack { Text(String(fullItem.ProductionYear)).font(.subheadline) .fontWeight(.medium) .foregroundColor(.secondary) @@ -437,19 +502,17 @@ struct EpisodeItemView: View { .fontWeight(.medium) .foregroundColor(.secondary) .lineLimit(1) - if(fullItem.OfficialRating != "") { + if fullItem.OfficialRating != "" { Text(fullItem.OfficialRating).font(.subheadline) .fontWeight(.semibold) .foregroundColor(.secondary) .lineLimit(1) .padding(EdgeInsets(top: 1, leading: 4, bottom: 1, trailing: 4)) - .overlay( - RoundedRectangle(cornerRadius: 2) - .stroke(Color.secondary, lineWidth: 1) - ) + .overlay(RoundedRectangle(cornerRadius: 2) + .stroke(Color.secondary, lineWidth: 1)) } - if(fullItem.CommunityRating != "0") { - HStack() { + if fullItem.CommunityRating != "0" { + HStack { Image(systemName: "star").foregroundColor(.secondary) Text(fullItem.CommunityRating).font(.subheadline) .fontWeight(.semibold) @@ -460,59 +523,79 @@ struct EpisodeItemView: View { } Spacer() }.frame(maxWidth: .infinity) - .offset(x: 14) + .offset(x: 14) }.frame(maxWidth: .infinity) Spacer() - HStack() { - Button() { + HStack { + Button { favorite.toggle() } label: { - if(!favorite) { - Image(systemName: "heart").foregroundColor(Color.primary).font(.system(size: 20)) + if !favorite { + Image(systemName: "heart").foregroundColor(Color.primary) + .font(.system(size: 20)) } else { - Image(systemName: "heart.fill").foregroundColor(Color(UIColor.systemRed)).font(.system(size: 20)) + Image(systemName: "heart.fill").foregroundColor(Color(UIColor.systemRed)) + .font(.system(size: 20)) } } - Button() { + Button { watched.toggle() } label: { - if(watched) { - Image(systemName: "checkmark.rectangle.fill").foregroundColor(Color.primary).font(.system(size: 20)) + if watched { + Image(systemName: "checkmark.rectangle.fill").foregroundColor(Color.primary) + .font(.system(size: 20)) } else { - Image(systemName: "xmark.rectangle").foregroundColor(Color.primary).font(.system(size: 20)) + Image(systemName: "xmark.rectangle").foregroundColor(Color.primary) + .font(.system(size: 20)) } } } }.padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) - if(fullItem.Tagline != "") { - Text(fullItem.Tagline).font(.body).italic().padding(.top, 3).fixedSize(horizontal: false, vertical: true).padding(.leading, 16).padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) + if fullItem.Tagline != "" { + Text(fullItem.Tagline).font(.body).italic().padding(.top, 3) + .fixedSize(horizontal: false, vertical: true).padding(.leading, 16) + .padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) } - Text(fullItem.Overview).font(.footnote).padding(.top, 3).fixedSize(horizontal: false, vertical: true).padding(.bottom, 3).padding(.leading, 16).padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) - if(fullItem.Genres.count != 0) { + Text(fullItem.Overview).font(.footnote).padding(.top, 3) + .fixedSize(horizontal: false, vertical: true).padding(.bottom, 3).padding(.leading, 16) + .padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) + if !fullItem.Genres.isEmpty { ScrollView(.horizontal, showsIndicators: false) { - HStack() { + HStack { Text("Genres:").font(.callout).fontWeight(.semibold) - ForEach(fullItem.Genres, id: \.Id) {genre in -// NavigationLink(destination: LibraryView(extraParams: "&Genres=\(genre.Name.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "")", title: genre.Name)) { + ForEach(fullItem.Genres, id: \.Id) { genre in + NavigationLink(destination: LibraryView(viewModel: .init(filter: Filter(genres: [ + genre + .Name, + ])), title: genre.Name)) { Text(genre.Name).font(.footnote) -// } + } } - }.padding(.leading, 16).padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) + }.padding(.leading, 16) + .padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) } } - if(fullItem.Cast.count != 0) { + if !fullItem.Cast.isEmpty { ScrollView(.horizontal, showsIndicators: false) { - VStack() { - Spacer().frame(height: 8); - HStack() { + VStack { + Spacer().frame(height: 8) + HStack { Spacer().frame(width: 16) ForEach(fullItem.Cast, id: \.Id) { cast in -// NavigationLink(destination: LibraryView(extraParams: "&PersonIds=\(cast.Id)", title: cast.Name)) { - VStack() { + NavigationLink(destination: LibraryView(viewModel: .init(filter: Filter(personIds: [ + cast + .Id, + ])), title: cast.Name)) { + VStack { WebImage(url: cast.Image) .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size .placeholder { - Image(uiImage: UIImage(blurHash: (cast.ImageBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : cast.ImageBlurHash), size: CGSize(width: 16, height: 16))!) + Image(uiImage: UIImage(blurHash: cast + .ImageBlurHash == "" ? + "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : + cast.ImageBlurHash, + size: CGSize(width: 16, + height: 16))!) .resizable() .aspectRatio(contentMode: .fill) .frame(width: 100, height: 100) @@ -521,12 +604,14 @@ struct EpisodeItemView: View { .aspectRatio(contentMode: .fill) .frame(width: 100, height: 100) .cornerRadius(10) - Text(cast.Name).font(.footnote).fontWeight(.regular).lineLimit(1).frame(width: 100).foregroundColor(Color.primary) - if(cast.Role != "") { - Text(cast.Role).font(.caption).fontWeight(.medium).lineLimit(1).foregroundColor(Color.secondary).frame(width: 100) + Text(cast.Name).font(.footnote).fontWeight(.regular).lineLimit(1) + .frame(width: 100).foregroundColor(Color.primary) + if cast.Role != "" { + Text(cast.Role).font(.caption).fontWeight(.medium).lineLimit(1) + .foregroundColor(Color.secondary).frame(width: 100) } } -// } + } Spacer().frame(width: 10) } Spacer().frame(width: UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) @@ -534,28 +619,35 @@ struct EpisodeItemView: View { } }.padding(.top, -3) } - if(fullItem.Directors.count != 0) { - HStack() { + if !fullItem.Directors.isEmpty { + HStack { Text("Directors:").font(.callout).fontWeight(.semibold) - Text(fullItem.Directors.joined(separator: ", ")).font(.footnote).lineLimit(1).foregroundColor(Color.secondary) - }.padding(.leading, 16).padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) + Text(fullItem.Directors.joined(separator: ", ")).font(.footnote).lineLimit(1) + .foregroundColor(Color.secondary) + }.padding(.leading, 16) + .padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) } - if(fullItem.Writers.count != 0) { - HStack() { + if !fullItem.Writers.isEmpty { + HStack { Text("Writers:").font(.callout).fontWeight(.semibold) - Text(fullItem.Writers.joined(separator: ", ")).font(.footnote).lineLimit(1).foregroundColor(Color.secondary) - }.padding(.leading, 16).padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) + Text(fullItem.Writers.joined(separator: ", ")).font(.footnote).lineLimit(1) + .foregroundColor(Color.secondary) + }.padding(.leading, 16) + .padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) } - if(fullItem.Studios.count != 0) { - HStack() { + if !fullItem.Studios.isEmpty { + HStack { Text("Studios:").font(.callout).fontWeight(.semibold) - Text(fullItem.Studios.joined(separator: ", ")).font(.footnote).lineLimit(1).foregroundColor(Color.secondary) - }.padding(.leading, 16).padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) + Text(fullItem.Studios.joined(separator: ", ")).font(.footnote).lineLimit(1) + .foregroundColor(Color.secondary) + }.padding(.leading, 16) + .padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) } - Spacer().frame(height: 100); + Spacer().frame(height: 100) }.frame(maxHeight: .infinity) } - }.padding(.top, 16).padding(.leading, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55).edgesIgnoringSafeArea(.leading) + }.padding(.top, 16).padding(.leading, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) + .edgesIgnoringSafeArea(.leading) } } } @@ -564,9 +656,9 @@ struct EpisodeItemView: View { .navigationBarTitleDisplayMode(.inline) .navigationTitle("\(fullItem.Name) - S\(String(fullItem.ParentIndexNumber ?? 0)):E\(String(fullItem.IndexNumber ?? 0)) - \(fullItem.SeriesName ?? "")") }.onAppear(perform: loadData) - .supportedOrientations(.allButUpsideDown) - .overrideViewPreference(.unspecified) - .preferredColorScheme(.none) - .prefersHomeIndicatorAutoHidden(false) + .supportedOrientations(.allButUpsideDown) + .overrideViewPreference(.unspecified) + .preferredColorScheme(.none) + .prefersHomeIndicatorAutoHidden(false) } } diff --git a/JellyfinPlayer/JellyApiTypings.swift b/JellyfinPlayer/JellyApiTypings.swift index 2373b382..9d584c7a 100644 --- a/JellyfinPlayer/JellyApiTypings.swift +++ b/JellyfinPlayer/JellyApiTypings.swift @@ -53,25 +53,25 @@ struct ServerAuthByNameResponse: Codable { var AccessToken: String } -class ResumeItem: ObservableObject { - @Published var Name: String = ""; - @Published var Id: String = ""; - @Published var IndexNumber: Int? = nil; - @Published var ParentIndexNumber: Int? = nil; - @Published var Image: String = ""; - @Published var ImageType: String = ""; - @Published var BlurHash: String = ""; - @Published var `Type`: String = ""; - @Published var SeasonId: String? = nil; - @Published var SeriesId: String? = nil; - @Published var SeriesName: String? = nil; - @Published var ItemProgress: Double = 0; - @Published var SeasonImage: String? = nil; - @Published var SeasonImageType: String? = nil; - @Published var SeasonImageBlurHash: String? = nil; - @Published var ItemBadge: Int? = 0; - @Published var ProductionYear: Int = 1999; - @Published var Watched: Bool = false; +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 { diff --git a/JellyfinPlayer/LatestMediaView.swift b/JellyfinPlayer/LatestMediaView.swift index 0c3d3c16..be8b48f9 100644 --- a/JellyfinPlayer/LatestMediaView.swift +++ b/JellyfinPlayer/LatestMediaView.swift @@ -47,7 +47,7 @@ struct LatestMediaView: View { let json = try JSON(data: body) for (_,item):(String, JSON) in json { // Do something you want - let itemObj = ResumeItem() + var itemObj = ResumeItem() itemObj.Image = item["ImageTags"]["Primary"].string ?? "" itemObj.ImageType = "Primary" itemObj.BlurHash = item["ImageBlurHashes"]["Primary"][itemObj.Image].string ?? "" diff --git a/JellyfinPlayer/LibraryFilterView.swift b/JellyfinPlayer/LibraryFilterView.swift index a16c4a5b..5ddaf6f3 100644 --- a/JellyfinPlayer/LibraryFilterView.swift +++ b/JellyfinPlayer/LibraryFilterView.swift @@ -14,173 +14,146 @@ struct Genre: Hashable, Identifiable { var id: String { name } } - struct LibraryFilterView: View { - @Environment(\.managedObjectContext) private var viewContext - @EnvironmentObject var globalData: GlobalData - - @State var library: String; - @Binding var output: String; - @State private var isLoading: Bool = true; - @State private var onlyUnplayed: Bool = false; - @State private var allGenres: [Genre] = []; - @State private var selectedGenres: Set = []; - - @State private var allRatings: [Genre] = []; - @State private var selectedRatings: Set = []; - @State private var sortBySelection: String = "SortName"; - @State private var sortOrder: String = "Descending"; - @State private var viewDidLoad: Bool = false; - @Binding var close: Bool; + @Environment(\.presentationMode) + var presentationMode + @Environment(\.managedObjectContext) + private var viewContext + @EnvironmentObject + var globalData: GlobalData + + @State + var library: String + @Binding + var filter: Filter + @State + private var isLoading: Bool = true + @State + private var onlyUnplayed: Bool = false + @State + private var allGenres: [Genre] = [] + @State + private var selectedGenres: Set = [] + + @State + private var allRatings: [Genre] = [] + @State + private var selectedRatings: Set = [] + @State + private var sortBySelection: String = "SortName" + @State + private var sortOrder: String = "Descending" + @State + private var viewDidLoad: Bool = false + func onAppear() { - if(_viewDidLoad.wrappedValue == true) { + if _viewDidLoad.wrappedValue == true { return } - _viewDidLoad.wrappedValue = true; - if(_output.wrappedValue.contains("&Filters=IsUnplayed")) { - _onlyUnplayed.wrappedValue = true; + _viewDidLoad.wrappedValue = true + if filter.filterTypes.contains(.isUnplayed) { + _onlyUnplayed.wrappedValue = true } - if(_output.wrappedValue.contains("&Genres=")) { - let genreString = _output.wrappedValue.components(separatedBy: "&Genres=")[1].components(separatedBy: "&")[0]; - for genre in genreString.components(separatedBy: "%7C") { - _selectedGenres.wrappedValue.insert(Genre(name: genre.removingPercentEncoding ?? "")) - } + if !filter.genres.isEmpty { + _selectedGenres.wrappedValue = Set(filter.genres.map { Genre(name: $0) }) } - if(_output.wrappedValue.contains("&OfficialRatings=")) { - let ratingString = _output.wrappedValue.components(separatedBy: "&OfficialRatings=")[1].components(separatedBy: "&")[0]; - for rating in ratingString.components(separatedBy: "%7C") { - _selectedRatings.wrappedValue.insert(Genre(name: rating.removingPercentEncoding ?? "")) - } + if !filter.officialRatings.isEmpty { + _selectedRatings.wrappedValue = Set(filter.officialRatings.map { Genre(name: $0) }) } - let sortBy = _output.wrappedValue.components(separatedBy: "&SortBy=")[1].components(separatedBy: "&")[0]; - _sortBySelection.wrappedValue = sortBy; - let sortOrder = _output.wrappedValue.components(separatedBy: "&SortOrder=")[1].components(separatedBy: "&")[0]; - _sortOrder.wrappedValue = sortOrder; - - recalculateFilters() + _sortBySelection.wrappedValue = filter.sort?.rawValue ?? sortBySelection + _sortOrder.wrappedValue = filter.asc?.rawValue ?? sortOrder + _allGenres.wrappedValue = [] let url = "/Items/Filters?UserId=\(globalData.user?.user_id ?? "")&ParentId=\(library)" let request = RestRequest(method: .get, url: (globalData.server?.baseURI ?? "") + url) request.headerParameters["X-Emby-Authorization"] = globalData.authHeader request.contentType = "application/json" request.acceptType = "application/json" - - request.responseData() { (result: Result, RestError>) in + + request.responseData { (result: Result, RestError>) in switch result { - case .success(let response): + case let .success(response): let body = response.body do { let json = try JSON(data: body) let arr = json["Genres"].arrayObject as? [String] ?? [] for genreName in arr { - //print(genreName) + // print(genreName) let genre = Genre(name: genreName) allGenres.append(genre) } - + let arr2 = json["OfficialRatings"].arrayObject as? [String] ?? [] for genreName in arr2 { - //print(genreName) + // print(genreName) let genre = Genre(name: genreName) allRatings.append(genre) } - } catch { - - } - break - case .failure(let error): + } catch {} + case let .failure(error): debugPrint(error) - break } - isLoading = false; + isLoading = false } } - - func recalculateFilters() { - print("recalcFilters running"); - output = ""; - if(_onlyUnplayed.wrappedValue) { - output = "&Filters=IsUnPlayed"; - } - - if(selectedGenres.count != 0) { - output += "&Genres=" - var genres: [String] = [] - for genre in selectedGenres { - genres.append(genre.name.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "") - } - output += genres.joined(separator: "%7C") - } - - if(selectedRatings.count != 0) { - output += "&OfficialRatings=" - var genres: [String] = [] - for genre in selectedRatings { - genres.append(genre.name.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "") - } - output += genres.joined(separator: "%7C") - } - output += "&SortBy=\(sortBySelection)&SortOrder=\(sortOrder)" - //print(output) - } - + var body: some View { - NavigationView() { + NavigationView { LoadingView(isShowing: $isLoading) { Form { Toggle("Only show unplayed items", isOn: $onlyUnplayed) - .onChange(of: onlyUnplayed) { tag in - recalculateFilters() + .onChange(of: onlyUnplayed) { value in + if value { + filter.filterTypes.append(.isUnplayed) + } else { + filter.filterTypes.removeAll { $0 == .isUnplayed } + } } - MultiSelector( - label: "Genres", - options: allGenres, - optionToString: { $0.name }, - selected: $selectedGenres - ).onChange(of: selectedGenres) { tag in - recalculateFilters() - } - MultiSelector( - label: "Parental Ratings", - options: allRatings, - optionToString: { $0.name }, - selected: $selectedRatings - ).onChange(of: selectedRatings) { tag in - recalculateFilters() - } - + MultiSelector(label: "Genres", + options: allGenres, + optionToString: { $0.name }, + selected: $selectedGenres) + .onChange(of: selectedGenres) { genres in + filter.genres = genres.map(\.id) + } + MultiSelector(label: "Parental Ratings", + options: allRatings, + optionToString: { $0.name }, + selected: $selectedRatings) + .onChange(of: selectedRatings) { ratings in + filter.officialRatings = ratings.map(\.id) + } + Section(header: Text("Sort settings")) { Picker("Sort by", selection: $sortBySelection) { Text("Name").tag("SortName") Text("Date Added").tag("DateCreated") Text("Date Played").tag("DatePlayed") Text("Date Released").tag("PremiereDate") - Text("Runtime").tag("Runtime") - }.onChange(of: sortBySelection) { tag in - recalculateFilters() + Text("Runtime").tag("Runtime") + }.onChange(of: sortBySelection) { value in + guard let sort = SortType(rawValue: value) else { return } + filter.sort = sort } Picker("Sort order", selection: $sortOrder) { Text("Ascending").tag("Ascending") Text("Descending").tag("Descending") - }.onChange(of: sortOrder) { tag in - recalculateFilters() + }.onChange(of: sortOrder) { order in + guard let asc = ASC(rawValue: order) else { return } + filter.asc = asc } } } }.onAppear(perform: onAppear) - .navigationBarTitle("Filters", displayMode: .inline) - .toolbar { - ToolbarItemGroup(placement: .navigationBarLeading) { - Button { - close = false - } label: { - HStack() { - Text("Back").font(.callout) - } + .navigationBarTitle("Filters", displayMode: .inline) + .navigationBarItems(leading: Button { + presentationMode.wrappedValue.dismiss() + } label: { + HStack { + Text("Back").font(.callout) } - } - } + }) } } } diff --git a/JellyfinPlayer/LibraryListView.swift b/JellyfinPlayer/LibraryListView.swift index 182e05ea..1b2ba3c8 100644 --- a/JellyfinPlayer/LibraryListView.swift +++ b/JellyfinPlayer/LibraryListView.swift @@ -40,26 +40,25 @@ struct LibraryListView: View { var body: some View { List(libraryIDs, id: \.self) { id in - if id != "genres" { - NavigationLink(destination: LibraryView(viewModel: .init(prefillID: id), title: libraryNames[id] ?? "")) { + switch id { + case "favorites": + NavigationLink(destination: LibraryView(viewModel: .init(filter: Filter(filterTypes: [.isFavorite])), + title: libraryNames[id] ?? "")) { Text(libraryNames[id] ?? "").foregroundColor(Color.primary) } - } else { - // NavigationLink(destination: LibraryView(prefill: id, names: libraryNames, libraries: library_ids)) { + case "genres": Text(libraryNames[id] ?? "").foregroundColor(Color.primary) - // } + default: + NavigationLink(destination: LibraryView(viewModel: .init(filter: Filter(parentID: id)), title: libraryNames[id] ?? "")) { + Text(libraryNames[id] ?? "").foregroundColor(Color.primary) + } } } .onAppear(perform: listOnAppear) .navigationTitle("All Media") - .toolbar { - ToolbarItemGroup(placement: .navigationBarTrailing) { -// NavigationLink(destination: LibrarySearchView(viewModel: .init(filter: .init()), -// close: $closeSearch), -// isActive: $closeSearch) { + .navigationBarItems(trailing: + NavigationLink(destination: LibrarySearchView(viewModel: .init(filter: .init()))) { Image(systemName: "magnifyingglass") -// } - } - } + }) } } diff --git a/JellyfinPlayer/LibrarySearchView.swift b/JellyfinPlayer/LibrarySearchView.swift index d32ce602..3a10c00e 100644 --- a/JellyfinPlayer/LibrarySearchView.swift +++ b/JellyfinPlayer/LibrarySearchView.swift @@ -15,31 +15,22 @@ struct LibrarySearchView: View { private var viewContext @EnvironmentObject var globalData: GlobalData - @ObservedObject var viewModel: LibrarySearchViewModel - - @Binding - var close: Bool + @State - var open: Bool = false - @State - private var onlyUnplayed: Bool = false - @State - private var viewDidLoad: Bool = false - @State - var linkedItem = ResumeItem() - - func onAppear() { - recalcTracks() - viewModel.globalData = globalData - } + private var tracks: [GridItem] = [] @Environment(\.verticalSizeClass) var verticalSizeClass: UserInterfaceSizeClass? @Environment(\.horizontalSizeClass) var horizontalSizeClass: UserInterfaceSizeClass? + func onAppear() { + recalcTracks() + viewModel.globalData = globalData + } + var isPortrait: Bool { let result = verticalSizeClass == .regular && horizontalSizeClass == .compact return result @@ -53,15 +44,9 @@ struct LibrarySearchView: View { } } - @State - private var tracks: [GridItem] = [] - var body: some View { ZStack { VStack { - NavigationLink(destination: ItemView(item: linkedItem), isActive: $open) { - EmptyView() - } Spacer().frame(height: 6) TextField("Search", text: $viewModel.searchQuery, onEditingChanged: { _ in print("changed") @@ -72,11 +57,7 @@ struct LibrarySearchView: View { ScrollView(.vertical) { LazyVGrid(columns: tracks) { ForEach(viewModel.items, id: \.Id) { item in - Button { - _linkedItem.wrappedValue = item - _close.wrappedValue = false - _open.wrappedValue = true - } label: { + NavigationLink(destination: ItemView(item: item)) { ResumeItemGridCell(item: item) } } @@ -87,6 +68,8 @@ struct LibrarySearchView: View { } if viewModel.isLoading { ActivityIndicator($viewModel.isLoading) + } else if viewModel.items.isEmpty { + Text("Empty Response") } } .onAppear(perform: onAppear) diff --git a/JellyfinPlayer/LibraryView.swift b/JellyfinPlayer/LibraryView.swift index d8ce2475..6904429b 100644 --- a/JellyfinPlayer/LibraryView.swift +++ b/JellyfinPlayer/LibraryView.swift @@ -18,28 +18,24 @@ struct LibraryView: View { @ObservedObject var viewModel: LibraryViewModel - @State - private var viewDidLoad: Bool = false @State private var showFiltersPopover: Bool = false @State private var showSearchPopover: Bool = false + + private var title: String + @State - private var title: String = "" - @State - private var closeSearch: Bool = false + private var tracks: [GridItem] = [] init(viewModel: LibraryViewModel, title: String) { self.viewModel = viewModel - self._title = State(initialValue: title) + self.title = title } func onAppear() { + recalcTracks() viewModel.globalData = globalData - if viewModel.items.isEmpty { - recalcTracks() - viewModel.requestInitItems() - } } @Environment(\.verticalSizeClass) @@ -60,11 +56,8 @@ struct LibraryView: View { } } - @State - private var tracks: [GridItem] = [] - var body: some View { - LoadingView(isShowing: $viewModel.isLoading) { + ZStack { ScrollView(.vertical) { Spacer().frame(height: 16) LazyVGrid(columns: tracks) { @@ -76,51 +69,46 @@ struct LibraryView: View { } Spacer().frame(height: 16) } - .gesture(DragGesture().onChanged { value in - if value.translation.height > 0 { - print("Scroll down") - } else { - print("Scroll up") - } - }) .onChange(of: isPortrait) { _ in recalcTracks() } + if viewModel.isLoading { + ActivityIndicator($viewModel.isLoading) + } else if viewModel.items.isEmpty { + Text("Empty Response") + } } .overrideViewPreference(.unspecified) .onAppear(perform: onAppear) .navigationTitle(title) - .toolbar { - ToolbarItemGroup(placement: .navigationBarTrailing) { - if !viewModel.isHiddenPreviousButton { - Button { - viewModel.requestPreviousPage() - } label: { - Image(systemName: "chevron.left") - } - } - if !viewModel.isHiddenNextButton { - Button { - viewModel.requestNextPage() - } label: { - Image(systemName: "chevron.right") - } - } - NavigationLink(destination: LibrarySearchView(viewModel: .init(filter: viewModel.filter), close: $closeSearch), - isActive: $closeSearch) { - Image(systemName: "magnifyingglass") - } + .navigationBarItems(trailing: HStack { + if !viewModel.isHiddenPreviousButton { Button { - showFiltersPopover = true + viewModel.requestPreviousPage() } label: { - Image(systemName: "line.horizontal.3.decrease") + Image(systemName: "chevron.left") } } + if !viewModel.isHiddenNextButton { + Button { + viewModel.requestNextPage() + } label: { + Image(systemName: "chevron.right") + } + } + NavigationLink(destination: LibrarySearchView(viewModel: .init(filter: viewModel.filter))) { + Image(systemName: "magnifyingglass") + } + Button { + showFiltersPopover = true + } label: { + Image(systemName: "line.horizontal.3.decrease") + } + }) + .sheet(isPresented: self.$showFiltersPopover) { + LibraryFilterView(library: viewModel.filter.parentID ?? "", filter: $viewModel.filter) + .environmentObject(self.globalData) } -// .sheet(isPresented: self.$showFiltersPopover) { -// LibraryFilterView(library: selected_library_id, output: $filterString, close: $showFiltersPopover) -// .environmentObject(self.globalData) -// } } } diff --git a/JellyfinPlayer/MovieItemView.swift b/JellyfinPlayer/MovieItemView.swift index 82ea2804..b7923310 100644 --- a/JellyfinPlayer/MovieItemView.swift +++ b/JellyfinPlayer/MovieItemView.swift @@ -5,140 +5,198 @@ // Created by Aiden Vigue on 5/13/21. // -import SwiftUI -import SwiftyRequest -import SwiftyJSON import SDWebImageSwiftUI +import SwiftUI +import SwiftyJSON +import SwiftyRequest class DetailItem: ObservableObject { - @Published var Name: String = ""; - @Published var Id: String = ""; - @Published var IndexNumber: Int? = nil; - @Published var ParentIndexNumber: Int? = nil; - @Published var Poster: String = ""; - @Published var Backdrop: String = "" - @Published var PosterBlurHash: String = ""; - @Published var BackdropBlurHash: String = ""; - @Published var `Type`: String = ""; - @Published var SeasonId: String? = nil; - @Published var SeriesId: String? = nil; - @Published var SeriesName: String? = nil; - @Published var ItemProgress: Double = 0; - @Published var ItemBadge: Int? = 0; - @Published var ProductionYear: Int = 1999; - @Published var Runtime: String = ""; - @Published var RuntimeTicks: Int = 0; - @Published var Cast: [CastMember] = []; - @Published var OfficialRating: String = ""; - @Published var Progress: Double = 0; - @Published var Watched: Bool = false; - @Published var Overview: String = ""; - @Published var Tagline: String = ""; - @Published var Directors: [String] = []; - @Published var Writers: [String] = []; - @Published var CriticRating: String = ""; - @Published var CommunityRating: String = ""; - @Published var Studios: [String] = []; - @Published var ParentId: String = ""; - @Published var Genres: [IVGenre] = []; - @Published var ProgressStr: String = ""; - @Published var ResumeItem: ResumeItem? = nil; - @Published var ParentBackdropItemId: String = ""; + @Published + var Name: String = "" + @Published + var Id: String = "" + @Published + var IndexNumber: Int? = nil + @Published + var ParentIndexNumber: Int? = nil + @Published + var Poster: String = "" + @Published + var Backdrop: String = "" + @Published + var PosterBlurHash: String = "" + @Published + var BackdropBlurHash: String = "" + @Published + var `Type`: String = "" + @Published + var SeasonId: String? = nil + @Published + var SeriesId: String? = nil + @Published + var SeriesName: String? = nil + @Published + var ItemProgress: Double = 0 + @Published + var ItemBadge: Int? = 0 + @Published + var ProductionYear: Int = 1999 + @Published + var Runtime: String = "" + @Published + var RuntimeTicks: Int = 0 + @Published + var Cast: [CastMember] = [] + @Published + var OfficialRating: String = "" + @Published + var Progress: Double = 0 + @Published + var Watched: Bool = false + @Published + var Overview: String = "" + @Published + var Tagline: String = "" + @Published + var Directors: [String] = [] + @Published + var Writers: [String] = [] + @Published + var CriticRating: String = "" + @Published + var CommunityRating: String = "" + @Published + var Studios: [String] = [] + @Published + var ParentId: String = "" + @Published + var Genres: [IVGenre] = [] + @Published + var ProgressStr: String = "" + @Published + var ResumeItem: ResumeItem? = nil + @Published + var ParentBackdropItemId: String = "" } class IVGenre: ObservableObject { - @Published var Id: String = ""; - @Published var Name: String = ""; + @Published + var Id: String = "" + @Published + var Name: String = "" } class CastMember: ObservableObject { - @Published var Name: String = ""; - @Published var Role: String = ""; - @Published var ImageBlurHash: String = ""; - @Published var Id: String = ""; - @Published var Image: URL = URL(string: "https://example.com")!; + @Published + var Name: String = "" + @Published + var Role: String = "" + @Published + var ImageBlurHash: String = "" + @Published + var Id: String = "" + @Published + var Image = URL(string: "https://example.com")! } struct MovieItemView: View { - @EnvironmentObject private var globalData: GlobalData - @EnvironmentObject private var orientationInfo: OrientationInfo - @EnvironmentObject private var playbackInfo: ItemPlayback - - @State private var isLoading: Bool = true; - - var item: ResumeItem; - var fullItem: DetailItem; - - @State private var progressString: String = ""; - @State private var viewDidLoad: Bool = false; - @State private var watched: Bool = false { + @EnvironmentObject + private var globalData: GlobalData + @EnvironmentObject + private var orientationInfo: OrientationInfo + @EnvironmentObject + private var playbackInfo: ItemPlayback + + @State + private var isLoading: Bool = true + + var item: ResumeItem + var fullItem: DetailItem + + @State + private var progressString: String = "" + @State + private var viewDidLoad: Bool = false + @State + private var watched: Bool = false { didSet { - if(watched == true) { + if watched == true { let date = Date() let formatter = DateFormatter() formatter.locale = Locale(identifier: "en_US_POSIX") formatter.timeZone = TimeZone(secondsFromGMT: 0) - print((globalData.server?.baseURI ?? "") + "/Users/\(globalData.user?.user_id ?? "")/PlayedItems/\(fullItem.Id)?DatePlayed=\(formatter.string(from: date).replacingOccurrences(of: ":", with: "%3A"))") - let request = RestRequest(method: .post, url: (globalData.server?.baseURI ?? "") + "/Users/\(globalData.user?.user_id ?? "")/PlayedItems/\(fullItem.Id)?DatePlayed=\(formatter.string(from: date).replacingOccurrences(of: ":", with: "%3A"))") + print((globalData.server?.baseURI ?? "") + + "/Users/\(globalData.user?.user_id ?? "")/PlayedItems/\(fullItem.Id)?DatePlayed=\(formatter.string(from: date).replacingOccurrences(of: ":", with: "%3A"))") + let request = RestRequest(method: .post, + url: (globalData.server?.baseURI ?? "") + + "/Users/\(globalData.user?.user_id ?? "")/PlayedItems/\(fullItem.Id)?DatePlayed=\(formatter.string(from: date).replacingOccurrences(of: ":", with: "%3A"))") request.headerParameters["X-Emby-Authorization"] = globalData.authHeader request.contentType = "application/json" request.acceptType = "application/json" - - request.responseData() { (result: Result, RestError>) in + + request.responseData { (_: Result, RestError>) in } } else { - let request = RestRequest(method: .delete, url: (globalData.server?.baseURI ?? "") + "/Users/\(globalData.user?.user_id ?? "")/PlayedItems/\(fullItem.Id)") + let request = RestRequest(method: .delete, + url: (globalData.server?.baseURI ?? "") + + "/Users/\(globalData.user?.user_id ?? "")/PlayedItems/\(fullItem.Id)") request.headerParameters["X-Emby-Authorization"] = globalData.authHeader request.contentType = "application/json" request.acceptType = "application/json" - - request.responseData() { (result: Result, RestError>) in + + request.responseData { (_: Result, RestError>) in } } } - }; - @State private var favorite: Bool = false { + } + + @State + private var favorite: Bool = false { didSet { - if(favorite == true) { - let request = RestRequest(method: .post, url: (globalData.server?.baseURI ?? "") + "/Users/\(globalData.user?.user_id ?? "")/FavoriteItems/\(fullItem.Id)") + if favorite == true { + let request = RestRequest(method: .post, + url: (globalData.server?.baseURI ?? "") + + "/Users/\(globalData.user?.user_id ?? "")/FavoriteItems/\(fullItem.Id)") request.headerParameters["X-Emby-Authorization"] = globalData.authHeader request.contentType = "application/json" request.acceptType = "application/json" - - request.responseData() { (result: Result, RestError>) in + + request.responseData { (_: Result, RestError>) in } } else { - let request = RestRequest(method: .delete, url: (globalData.server?.baseURI ?? "") + "/Users/\(globalData.user?.user_id ?? "")/FavoriteItems/\(fullItem.Id)") + let request = RestRequest(method: .delete, + url: (globalData.server?.baseURI ?? "") + + "/Users/\(globalData.user?.user_id ?? "")/FavoriteItems/\(fullItem.Id)") request.headerParameters["X-Emby-Authorization"] = globalData.authHeader request.contentType = "application/json" request.acceptType = "application/json" - - request.responseData() { (result: Result, RestError>) in + + request.responseData { (_: Result, RestError>) in } } } - }; - + } + init(item: ResumeItem) { - self.item = item; - fullItem = DetailItem(); + self.item = item + self.fullItem = DetailItem() } func loadData() { - if(_viewDidLoad.wrappedValue == true) { - return; + if _viewDidLoad.wrappedValue == true { + return } - _viewDidLoad.wrappedValue = true; + _viewDidLoad.wrappedValue = true let url = "/Users/\(globalData.user?.user_id ?? "")/Items/\(item.Id)" - + let request = RestRequest(method: .get, url: (globalData.server?.baseURI ?? "") + url) request.headerParameters["X-Emby-Authorization"] = globalData.authHeader request.contentType = "application/json" request.acceptType = "application/json" - - request.responseData() { (result: Result, RestError>) in + + request.responseData { (result: Result, RestError>) in switch result { - case .success(let response): + case let .success(response): let body = response.body do { let json = try JSON(data: body) @@ -159,224 +217,255 @@ struct MovieItemView: View { fullItem.SeriesName = json["SeriesName"].string ?? nil fullItem.Progress = Double(json["UserData"]["PlaybackPositionTicks"].int ?? 0) fullItem.OfficialRating = json["OfficialRating"].string ?? "PG-13" - fullItem.Watched = json["UserData"]["Played"].bool ?? false; - fullItem.CommunityRating = String(json["CommunityRating"].float ?? 0.0); - fullItem.CriticRating = String(json["CriticRating"].int ?? 0); + fullItem.Watched = json["UserData"]["Played"].bool ?? false + fullItem.CommunityRating = String(json["CommunityRating"].float ?? 0.0) + fullItem.CriticRating = String(json["CriticRating"].int ?? 0) fullItem.ParentId = json["ParentId"].string ?? "" - //People + // People fullItem.Directors = [] fullItem.Studios = [] fullItem.Writers = [] fullItem.Cast = [] fullItem.Genres = [] - - for (_,person):(String, JSON) in json["People"] { - if(person["Type"].stringValue == "Director") { - fullItem.Directors.append(person["Name"].string ?? ""); - } else if(person["Type"].stringValue == "Writer") { - fullItem.Writers.append(person["Name"].string ?? ""); - } else if(person["Type"].stringValue == "Actor") { - let cast = CastMember(); - cast.Name = person["Name"].string ?? ""; - cast.Id = person["Id"].string ?? ""; - let imageTag = person["PrimaryImageTag"].string ?? ""; - cast.ImageBlurHash = person["ImageBlurHashes"]["Primary"][imageTag].string ?? ""; - cast.Role = person["Role"].string ?? ""; - cast.Image = URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(cast.Id)/Images/Primary?maxWidth=250&quality=85&tag=\(imageTag)")! - fullItem.Cast.append(cast); + + for (_, person): (String, JSON) in json["People"] { + if person["Type"].stringValue == "Director" { + fullItem.Directors.append(person["Name"].string ?? "") + } else if person["Type"].stringValue == "Writer" { + fullItem.Writers.append(person["Name"].string ?? "") + } else if person["Type"].stringValue == "Actor" { + let cast = CastMember() + cast.Name = person["Name"].string ?? "" + cast.Id = person["Id"].string ?? "" + let imageTag = person["PrimaryImageTag"].string ?? "" + cast.ImageBlurHash = person["ImageBlurHashes"]["Primary"][imageTag].string ?? "" + cast.Role = person["Role"].string ?? "" + cast + .Image = + URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(cast.Id)/Images/Primary?maxWidth=250&quality=85&tag=\(imageTag)")! + fullItem.Cast.append(cast) } } - - //Studios - for (_,studio):(String, JSON) in json["Studios"] { - fullItem.Studios.append(studio["Name"].string ?? ""); + + // Studios + for (_, studio): (String, JSON) in json["Studios"] { + fullItem.Studios.append(studio["Name"].string ?? "") } - - //Genres - for (_,genre):(String, JSON) in json["GenreItems"] { + + // Genres + for (_, genre): (String, JSON) in json["GenreItems"] { let tmpGenre = IVGenre() - tmpGenre.Id = genre["Id"].string ?? ""; - tmpGenre.Name = genre["Name"].string ?? ""; - fullItem.Genres.append(tmpGenre); + tmpGenre.Id = genre["Id"].string ?? "" + tmpGenre.Name = genre["Name"].string ?? "" + fullItem.Genres.append(tmpGenre) } - + _watched.wrappedValue = fullItem.Watched - _favorite.wrappedValue = json["UserData"]["IsFavorite"].bool ?? false; - - //Process runtime - let seconds: Int = ((json["RunTimeTicks"].int ?? 0)/10000000) - fullItem.RuntimeTicks = json["RunTimeTicks"].int ?? 0; - let hours = (seconds/3600) - let minutes = ((seconds - (hours * 3600))/60) - if(hours != 0) { + _favorite.wrappedValue = json["UserData"]["IsFavorite"].bool ?? false + + // Process runtime + let seconds: Int = ((json["RunTimeTicks"].int ?? 0) / 10_000_000) + fullItem.RuntimeTicks = json["RunTimeTicks"].int ?? 0 + let hours = (seconds / 3600) + let minutes = ((seconds - (hours * 3600)) / 60) + if hours != 0 { fullItem.Runtime = "\(hours):\(String(minutes).leftPad(toWidth: 2, withString: "0"))" } else { fullItem.Runtime = "\(String(minutes).leftPad(toWidth: 2, withString: "0"))m" } - - if(fullItem.Progress != 0) { - let remainingSecs = (Double(json["RunTimeTicks"].int ?? 0) - fullItem.Progress)/10000000 - let proghours = Int(remainingSecs/3600) - let progminutes = Int((Int(remainingSecs) - (proghours * 3600))/60) - if(proghours != 0) { + + if fullItem.Progress != 0 { + let remainingSecs = (Double(json["RunTimeTicks"].int ?? 0) - fullItem.Progress) / 10_000_000 + let proghours = Int(remainingSecs / 3600) + let progminutes = Int((Int(remainingSecs) - (proghours * 3600)) / 60) + if proghours != 0 { _progressString.wrappedValue = "\(proghours):\(String(progminutes).leftPad(toWidth: 2, withString: "0"))" } else { _progressString.wrappedValue = "\(String(progminutes).leftPad(toWidth: 2, withString: "0"))m" } } - _isLoading.wrappedValue = false; - } catch { - - } - break - case .failure(let error): + _isLoading.wrappedValue = false + } catch {} + case let .failure(error): debugPrint(error) - break } } } - + var body: some View { LoadingView(isShowing: $isLoading) { - VStack(alignment:.leading) { - if(!isLoading) { - if(orientationInfo.orientation == .portrait) { + VStack(alignment: .leading) { + if !isLoading { + if orientationInfo.orientation == .portrait { GeometryReader { geometry in - VStack() { + VStack { WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.Id)/Images/Backdrop?maxWidth=550&quality=90&tag=\(fullItem.Backdrop)")!) .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size .placeholder { - Image(uiImage: UIImage(blurHash: (fullItem.BackdropBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : fullItem.BackdropBlurHash), size: CGSize(width: 32, height: 32))!) + Image(uiImage: UIImage(blurHash: fullItem + .BackdropBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : fullItem + .BackdropBlurHash, + size: CGSize(width: 32, height: 32))!) .resizable() - .frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing, height: UIDevice.current.userInterfaceIdiom == .pad ? 350 : (geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing) * 0.5625) + .frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets + .trailing, + height: UIDevice.current + .userInterfaceIdiom == .pad ? 350 : + (geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets + .trailing) * 0.5625) } - + .opacity(0.3) .aspectRatio(contentMode: .fill) - .frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing, height: UIDevice.current.userInterfaceIdiom == .pad ? 350 : (geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing) * 0.5625) + .frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing, + height: UIDevice.current + .userInterfaceIdiom == .pad ? 350 : + (geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing) * + 0.5625) .shadow(radius: 5) - .overlay( - HStack() { - WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.Id)/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)")!) - .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size - .placeholder { - Image(uiImage: UIImage(blurHash: (fullItem.PosterBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : fullItem.PosterBlurHash), size: CGSize(width: 32, height: 32))!) - .resizable() - .frame(width: 120, height: 180) - .cornerRadius(10) - }.aspectRatio(contentMode: .fill) - .frame(width: 120, height: 180) - .cornerRadius(10) - VStack(alignment: .leading) { - Spacer() - Text(fullItem.Name).font(.headline) - .fontWeight(.semibold) - .foregroundColor(.primary) - .fixedSize(horizontal: false, vertical: true) - .offset(y: -4) - HStack() { - Text(String(fullItem.ProductionYear)).font(.subheadline) - .fontWeight(.medium) + .overlay(HStack { + WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.Id)/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)")!) + .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size + .placeholder { + Image(uiImage: UIImage(blurHash: fullItem + .PosterBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : + fullItem.PosterBlurHash, + size: CGSize(width: 32, height: 32))!) + .resizable() + .frame(width: 120, height: 180) + .cornerRadius(10) + }.aspectRatio(contentMode: .fill) + .frame(width: 120, height: 180) + .cornerRadius(10) + VStack(alignment: .leading) { + Spacer() + Text(fullItem.Name).font(.headline) + .fontWeight(.semibold) + .foregroundColor(.primary) + .fixedSize(horizontal: false, vertical: true) + .offset(y: -4) + HStack { + Text(String(fullItem.ProductionYear)).font(.subheadline) + .fontWeight(.medium) + .foregroundColor(.secondary) + .lineLimit(1) + Text(fullItem.Runtime).font(.subheadline) + .fontWeight(.medium) + .foregroundColor(.secondary) + .lineLimit(1) + if fullItem.OfficialRating != "" { + Text(fullItem.OfficialRating).font(.subheadline) + .fontWeight(.semibold) .foregroundColor(.secondary) .lineLimit(1) - Text(fullItem.Runtime).font(.subheadline) - .fontWeight(.medium) - .foregroundColor(.secondary) - .lineLimit(1) - if(fullItem.OfficialRating != "") { - Text(fullItem.OfficialRating).font(.subheadline) + .padding(EdgeInsets(top: 1, leading: 4, bottom: 1, trailing: 4)) + .overlay(RoundedRectangle(cornerRadius: 2) + .stroke(Color.secondary, lineWidth: 1)) + } + if fullItem.CommunityRating != "0" { + HStack { + Image(systemName: "star").foregroundColor(.secondary) + Text(fullItem.CommunityRating).font(.subheadline) .fontWeight(.semibold) .foregroundColor(.secondary) .lineLimit(1) - .padding(EdgeInsets(top: 1, leading: 4, bottom: 1, trailing: 4)) - .overlay( - RoundedRectangle(cornerRadius: 2) - .stroke(Color.secondary, lineWidth: 1) - ) + .offset(x: -7, y: 0.7) } - if(fullItem.CommunityRating != "0") { - HStack() { - Image(systemName: "star").foregroundColor(.secondary) - Text(fullItem.CommunityRating).font(.subheadline) - .fontWeight(.semibold) - .foregroundColor(.secondary) - .lineLimit(1) - .offset(x: -7, y: 0.7) - } - } - }.frame(maxWidth: .infinity, alignment: .leading) - }.offset(x: 0, y: UIDevice.current.userInterfaceIdiom == .pad ? -98 : -46).padding(.trailing, 16) - }.offset(x: 16, y: UIDevice.current.userInterfaceIdiom == .pad ? 135 : 40) - , alignment: .bottomLeading) + } + }.frame(maxWidth: .infinity, alignment: .leading) + }.offset(x: 0, y: UIDevice.current.userInterfaceIdiom == .pad ? -98 : -46) + .padding(.trailing, 16) + }.offset(x: 16, y: UIDevice.current.userInterfaceIdiom == .pad ? 135 : 40), + alignment: .bottomLeading) VStack(alignment: .leading) { - HStack() { - //Play button + HStack { + // Play button Button { - self.playbackInfo.itemToPlay = fullItem; - self.playbackInfo.shouldPlay = true; + self.playbackInfo.itemToPlay = fullItem + self.playbackInfo.shouldPlay = true } label: { - HStack() { - Text(fullItem.Progress == 0 ? "Play" : "\(progressString) left").foregroundColor(Color.white).font(.callout).fontWeight(.semibold) + HStack { + Text(fullItem.Progress == 0 ? "Play" : "\(progressString) left") + .foregroundColor(Color.white).font(.callout).fontWeight(.semibold) Image(systemName: "play.fill").foregroundColor(Color.white).font(.system(size: 20)) } .frame(width: 120, height: 35) - .background(Color(red: 172/255, green: 92/255, blue: 195/255)) + .background(Color(red: 172 / 255, green: 92 / 255, blue: 195 / 255)) .cornerRadius(10) }.buttonStyle(PlainButtonStyle()) - .frame(width: 120, height: 35) + .frame(width: 120, height: 35) Spacer() - HStack() { - Button() { + HStack { + Button { favorite.toggle() } label: { - if(!favorite) { + if !favorite { Image(systemName: "heart").foregroundColor(Color.primary).font(.system(size: 20)) } else { - Image(systemName: "heart.fill").foregroundColor(Color(UIColor.systemRed)).font(.system(size: 20)) + Image(systemName: "heart.fill").foregroundColor(Color(UIColor.systemRed)) + .font(.system(size: 20)) } } - Button() { + Button { watched.toggle() } label: { - if(watched) { - Image(systemName: "checkmark.rectangle.fill").foregroundColor(Color.primary).font(.system(size: 20)) + if watched { + Image(systemName: "checkmark.rectangle.fill").foregroundColor(Color.primary) + .font(.system(size: 20)) } else { - Image(systemName: "xmark.rectangle").foregroundColor(Color.primary).font(.system(size: 20)) + Image(systemName: "xmark.rectangle").foregroundColor(Color.primary) + .font(.system(size: 20)) } } } - }.padding(.leading, 16).padding(.trailing,16) - ScrollView() { + }.padding(.leading, 16).padding(.trailing, 16) + ScrollView { VStack(alignment: .leading) { - if(fullItem.Tagline != "") { - Text(fullItem.Tagline).font(.body).italic().padding(.top, 7).fixedSize(horizontal: false, vertical: true).padding(.leading, 16).padding(.trailing,16) + if fullItem.Tagline != "" { + Text(fullItem.Tagline).font(.body).italic().padding(.top, 7) + .fixedSize(horizontal: false, vertical: true).padding(.leading, 16) + .padding(.trailing, 16) } - Text(fullItem.Overview).font(.footnote).padding(.top, 3).fixedSize(horizontal: false, vertical: true).padding(.bottom, 3).padding(.leading, 16).padding(.trailing,16) - if(fullItem.Genres.count != 0) { + Text(fullItem.Overview).font(.footnote).padding(.top, 3) + .fixedSize(horizontal: false, vertical: true).padding(.bottom, 3).padding(.leading, 16) + .padding(.trailing, 16) + if !fullItem.Genres.isEmpty { ScrollView(.horizontal, showsIndicators: false) { - HStack() { + HStack { Text("Genres:").font(.callout).fontWeight(.semibold) - ForEach(fullItem.Genres, id: \.Id) {genre in -// NavigationLink(destination: LibraryView(extraParams: "&Genres=\(genre.Name.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "")", title: genre.Name)) { + ForEach(fullItem.Genres, id: \.Id) { genre in + NavigationLink(destination: LibraryView(viewModel: .init(filter: Filter(genres: [ + genre + .Name, + ])), + title: genre.Name)) { Text(genre.Name).font(.footnote) -// } + } } - }.padding(.leading, 16).padding(.trailing,16) + }.padding(.leading, 16).padding(.trailing, 16) } } - if(fullItem.Cast.count != 0) { + if !fullItem.Cast.isEmpty { ScrollView(.horizontal, showsIndicators: false) { - VStack() { - Spacer().frame(height: 8); - HStack() { + VStack { + Spacer().frame(height: 8) + HStack { Spacer().frame(width: 16) ForEach(fullItem.Cast, id: \.Id) { cast in -// NavigationLink(destination: LibraryView(extraParams: "&PersonIds=\(cast.Id)", title: cast.Name)) { - VStack() { + NavigationLink(destination: LibraryView(viewModel: .init(filter: Filter(personIds: [ + cast + .Id, + ])), title: cast.Name)) { + VStack { WebImage(url: cast.Image) .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size .placeholder { - Image(uiImage: UIImage(blurHash: (cast.ImageBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : cast.ImageBlurHash), size: CGSize(width: 16, height: 16))!) + Image(uiImage: UIImage(blurHash: cast + .ImageBlurHash == "" ? + "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : + cast.ImageBlurHash, + size: CGSize(width: 16, + height: 16))!) .resizable() .aspectRatio(contentMode: .fill) .frame(width: 100, height: 100) @@ -385,12 +474,14 @@ struct MovieItemView: View { .aspectRatio(contentMode: .fill) .frame(width: 100, height: 100) .cornerRadius(10) - Text(cast.Name).font(.footnote).fontWeight(.regular).lineLimit(1).frame(width: 100).foregroundColor(Color.primary) - if(cast.Role != "") { - Text(cast.Role).font(.caption).fontWeight(.medium).lineLimit(1).foregroundColor(Color.secondary).frame(width: 100) + Text(cast.Name).font(.footnote).fontWeight(.regular).lineLimit(1) + .frame(width: 100).foregroundColor(Color.primary) + if cast.Role != "" { + Text(cast.Role).font(.caption).fontWeight(.medium).lineLimit(1) + .foregroundColor(Color.secondary).frame(width: 100) } } -// } + } Spacer().frame(width: 10) } Spacer().frame(width: 16) @@ -398,51 +489,66 @@ struct MovieItemView: View { } }.padding(.top, -3) } - if(fullItem.Directors.count != 0) { - HStack() { + if !fullItem.Directors.isEmpty { + HStack { Text("Directors:").font(.callout).fontWeight(.semibold) - Text(fullItem.Directors.joined(separator: ", ")).font(.footnote).lineLimit(1).foregroundColor(Color.secondary) - }.padding(.leading, 16).padding(.trailing,16) + Text(fullItem.Directors.joined(separator: ", ")).font(.footnote).lineLimit(1) + .foregroundColor(Color.secondary) + }.padding(.leading, 16).padding(.trailing, 16) } - if(fullItem.Writers.count != 0) { - HStack() { + if !fullItem.Writers.isEmpty { + HStack { Text("Writers:").font(.callout).fontWeight(.semibold) - Text(fullItem.Writers.joined(separator: ", ")).font(.footnote).lineLimit(1).foregroundColor(Color.secondary) - }.padding(.leading, 16).padding(.trailing,16) + Text(fullItem.Writers.joined(separator: ", ")).font(.footnote).lineLimit(1) + .foregroundColor(Color.secondary) + }.padding(.leading, 16).padding(.trailing, 16) } - if(fullItem.Studios.count != 0) { - HStack() { + if !fullItem.Studios.isEmpty { + HStack { Text("Studios:").font(.callout).fontWeight(.semibold) - Text(fullItem.Studios.joined(separator: ", ")).font(.footnote).lineLimit(1).foregroundColor(Color.secondary) - }.padding(.leading, 16).padding(.trailing,16) + Text(fullItem.Studios.joined(separator: ", ")).font(.footnote).lineLimit(1) + .foregroundColor(Color.secondary) + }.padding(.leading, 16).padding(.trailing, 16) } Spacer().frame(height: 3) } } - }.padding(EdgeInsets(top: UIDevice.current.userInterfaceIdiom == .pad ? 54 : 24, leading: 0, bottom: 0, trailing: 0)) + } + .padding(EdgeInsets(top: UIDevice.current.userInterfaceIdiom == .pad ? 54 : 24, leading: 0, bottom: 0, + trailing: 0)) } } } else { GeometryReader { geometry in - ZStack() { + ZStack { WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.Id)/Images/Backdrop?maxWidth=750&quality=90&tag=\(fullItem.Backdrop)")!) .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size .placeholder { - Image(uiImage: UIImage(blurHash: (fullItem.BackdropBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : fullItem.BackdropBlurHash), size: CGSize(width: 16, height: 16))!) + Image(uiImage: UIImage(blurHash: fullItem + .BackdropBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : fullItem + .BackdropBlurHash, + size: CGSize(width: 16, height: 16))!) .resizable() - .frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing, height: geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom) + .frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets + .trailing, + height: geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets + .bottom) } - + .opacity(0.3) .aspectRatio(contentMode: .fill) - .frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing, height: geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom) + .frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing, + height: geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom) .edgesIgnoringSafeArea(.all) - HStack() { - VStack() { + HStack { + VStack { WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.Id)/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)")!) .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size .placeholder { - Image(uiImage: UIImage(blurHash: (fullItem.PosterBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : fullItem.PosterBlurHash), size: CGSize(width: 16, height: 16))!) + Image(uiImage: UIImage(blurHash: fullItem + .PosterBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : + fullItem.PosterBlurHash, + size: CGSize(width: 16, height: 16))!) .resizable() .frame(width: 120, height: 180) } @@ -451,23 +557,24 @@ struct MovieItemView: View { .shadow(radius: 5) Spacer().frame(height: 15) Button { - self.playbackInfo.itemToPlay = fullItem; - self.playbackInfo.shouldPlay = true; + self.playbackInfo.itemToPlay = fullItem + self.playbackInfo.shouldPlay = true } label: { - HStack() { - Text(fullItem.Progress == 0 ? "Play" : "\(progressString) left").foregroundColor(Color.white).font(.callout).fontWeight(.semibold) + HStack { + Text(fullItem.Progress == 0 ? "Play" : "\(progressString) left") + .foregroundColor(Color.white).font(.callout).fontWeight(.semibold) Image(systemName: "play.fill").foregroundColor(Color.white).font(.system(size: 20)) } .frame(width: 120, height: 35) - .background(Color(red: 172/255, green: 92/255, blue: 195/255)) + .background(Color(red: 172 / 255, green: 92 / 255, blue: 195 / 255)) .cornerRadius(10) }.buttonStyle(PlainButtonStyle()) - .frame(width: 120, height: 35) + .frame(width: 120, height: 35) Spacer() } - ScrollView() { + ScrollView { VStack(alignment: .leading) { - HStack() { + HStack { VStack(alignment: .leading) { Text(fullItem.Name).font(.headline) .fontWeight(.semibold) @@ -475,7 +582,7 @@ struct MovieItemView: View { .fixedSize(horizontal: false, vertical: true) .offset(x: 14, y: 0) Spacer().frame(height: 1) - HStack() { + HStack { Text(String(fullItem.ProductionYear)).font(.subheadline) .fontWeight(.medium) .foregroundColor(.secondary) @@ -484,19 +591,17 @@ struct MovieItemView: View { .fontWeight(.medium) .foregroundColor(.secondary) .lineLimit(1) - if(fullItem.OfficialRating != "") { + if fullItem.OfficialRating != "" { Text(fullItem.OfficialRating).font(.subheadline) .fontWeight(.semibold) .foregroundColor(.secondary) .lineLimit(1) .padding(EdgeInsets(top: 1, leading: 4, bottom: 1, trailing: 4)) - .overlay( - RoundedRectangle(cornerRadius: 2) - .stroke(Color.secondary, lineWidth: 1) - ) + .overlay(RoundedRectangle(cornerRadius: 2) + .stroke(Color.secondary, lineWidth: 1)) } - if(fullItem.CommunityRating != "0") { - HStack() { + if fullItem.CommunityRating != "0" { + HStack { Image(systemName: "star").foregroundColor(.secondary) Text(fullItem.CommunityRating).font(.subheadline) .fontWeight(.semibold) @@ -507,59 +612,80 @@ struct MovieItemView: View { } Spacer() }.frame(maxWidth: .infinity, alignment: .leading) - .offset(x: 14) + .offset(x: 14) }.frame(maxWidth: .infinity, alignment: .leading) Spacer() - HStack() { - Button() { + HStack { + Button { favorite.toggle() } label: { - if(!favorite) { - Image(systemName: "heart").foregroundColor(Color.primary).font(.system(size: 20)) + if !favorite { + Image(systemName: "heart").foregroundColor(Color.primary) + .font(.system(size: 20)) } else { - Image(systemName: "heart.fill").foregroundColor(Color(UIColor.systemRed)).font(.system(size: 20)) + Image(systemName: "heart.fill").foregroundColor(Color(UIColor.systemRed)) + .font(.system(size: 20)) } } - Button() { + Button { watched.toggle() } label: { - if(watched) { - Image(systemName: "checkmark.rectangle.fill").foregroundColor(Color.primary).font(.system(size: 20)) + if watched { + Image(systemName: "checkmark.rectangle.fill").foregroundColor(Color.primary) + .font(.system(size: 20)) } else { - Image(systemName: "xmark.rectangle").foregroundColor(Color.primary).font(.system(size: 20)) + Image(systemName: "xmark.rectangle").foregroundColor(Color.primary) + .font(.system(size: 20)) } } } }.padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) - if(fullItem.Tagline != "") { - Text(fullItem.Tagline).font(.body).italic().padding(.top, 3).fixedSize(horizontal: false, vertical: true).padding(.leading, 16).padding(.trailing,UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) + if fullItem.Tagline != "" { + Text(fullItem.Tagline).font(.body).italic().padding(.top, 3) + .fixedSize(horizontal: false, vertical: true).padding(.leading, 16) + .padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) } - Text(fullItem.Overview).font(.footnote).padding(.top, 3).fixedSize(horizontal: false, vertical: true).padding(.bottom, 3).padding(.leading, 16).padding(.trailing,UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) - if(fullItem.Genres.count != 0) { + Text(fullItem.Overview).font(.footnote).padding(.top, 3) + .fixedSize(horizontal: false, vertical: true).padding(.bottom, 3).padding(.leading, 16) + .padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) + if !fullItem.Genres.isEmpty { ScrollView(.horizontal, showsIndicators: false) { - HStack() { + HStack { Text("Genres:").font(.callout).fontWeight(.semibold) - ForEach(fullItem.Genres, id: \.Id) {genre in -// NavigationLink(destination: LibraryView(extraParams: "&Genres=\(genre.Name.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "")", title: genre.Name)) { + ForEach(fullItem.Genres, id: \.Id) { genre in + NavigationLink(destination: LibraryView(viewModel: .init(filter: Filter(genres: [ + genre + .Name, + ])), + title: genre.Name)) { Text(genre.Name).font(.footnote) -// } + } } - }.padding(.leading, 16).padding(.trailing,UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) + }.padding(.leading, 16) + .padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) } } - if(fullItem.Cast.count != 0) { + if !fullItem.Cast.isEmpty { ScrollView(.horizontal, showsIndicators: false) { - VStack() { - Spacer().frame(height: 8); - HStack() { + VStack { + Spacer().frame(height: 8) + HStack { Spacer().frame(width: 16) ForEach(fullItem.Cast, id: \.Id) { cast in -// NavigationLink(destination: LibraryView(extraParams: "&PersonIds=\(cast.Id)", title: cast.Name)) { - VStack() { + NavigationLink(destination: LibraryView(viewModel: .init(filter: Filter(personIds: [ + cast + .Id, + ])), title: cast.Name)) { + VStack { WebImage(url: cast.Image) .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size .placeholder { - Image(uiImage: UIImage(blurHash: (cast.ImageBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : cast.ImageBlurHash), size: CGSize(width: 16, height: 16))!) + Image(uiImage: UIImage(blurHash: cast + .ImageBlurHash == "" ? + "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : + cast.ImageBlurHash, + size: CGSize(width: 16, + height: 16))!) .resizable() .aspectRatio(contentMode: .fill) .frame(width: 100, height: 100) @@ -568,41 +694,51 @@ struct MovieItemView: View { .aspectRatio(contentMode: .fill) .frame(width: 100, height: 100) .cornerRadius(10) - Text(cast.Name).font(.footnote).fontWeight(.regular).lineLimit(1).frame(width: 100).foregroundColor(Color.primary) - if(cast.Role != "") { - Text(cast.Role).font(.caption).fontWeight(.medium).lineLimit(1).foregroundColor(Color.secondary).frame(width: 100) + Text(cast.Name).font(.footnote).fontWeight(.regular).lineLimit(1) + .frame(width: 100).foregroundColor(Color.primary) + if cast.Role != "" { + Text(cast.Role).font(.caption).fontWeight(.medium).lineLimit(1) + .foregroundColor(Color.secondary).frame(width: 100) } } -// } + } Spacer().frame(width: 10) } Spacer().frame(width: UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) } } - }.padding(.top, -3).padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? -55 : 0) + }.padding(.top, -3) + .padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? -55 : 0) } - if(fullItem.Directors.count != 0) { - HStack() { + if !fullItem.Directors.isEmpty { + HStack { Text("Directors:").font(.callout).fontWeight(.semibold) - Text(fullItem.Directors.joined(separator: ", ")).font(.footnote).lineLimit(1).foregroundColor(Color.secondary) - }.padding(.leading, 16).padding(.trailing,UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) + Text(fullItem.Directors.joined(separator: ", ")).font(.footnote).lineLimit(1) + .foregroundColor(Color.secondary) + }.padding(.leading, 16) + .padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) } - if(fullItem.Writers.count != 0) { - HStack() { + if !fullItem.Writers.isEmpty { + HStack { Text("Writers:").font(.callout).fontWeight(.semibold) - Text(fullItem.Writers.joined(separator: ", ")).font(.footnote).lineLimit(1).foregroundColor(Color.secondary) - }.padding(.leading, 16).padding(.trailing,UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) + Text(fullItem.Writers.joined(separator: ", ")).font(.footnote).lineLimit(1) + .foregroundColor(Color.secondary) + }.padding(.leading, 16) + .padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) } - if(fullItem.Studios.count != 0) { - HStack() { + if !fullItem.Studios.isEmpty { + HStack { Text("Studios:").font(.callout).fontWeight(.semibold) - Text(fullItem.Studios.joined(separator: ", ")).font(.footnote).lineLimit(1).foregroundColor(Color.secondary) - }.padding(.leading, 16).padding(.trailing,UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) + Text(fullItem.Studios.joined(separator: ", ")).font(.footnote).lineLimit(1) + .foregroundColor(Color.secondary) + }.padding(.leading, 16) + .padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) } - Spacer().frame(height: 195); + Spacer().frame(height: 195) }.frame(maxHeight: .infinity) } - }.padding(.top, 16).padding(.leading, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55).edgesIgnoringSafeArea(.leading) + }.padding(.top, 16).padding(.leading, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) + .edgesIgnoringSafeArea(.leading) } } } @@ -611,9 +747,9 @@ struct MovieItemView: View { .navigationBarTitleDisplayMode(.inline) .navigationTitle(fullItem.Name) }.onAppear(perform: loadData) - .supportedOrientations(.allButUpsideDown) - .overrideViewPreference(.unspecified) - .preferredColorScheme(.none) - .prefersHomeIndicatorAutoHidden(false) + .supportedOrientations(.allButUpsideDown) + .overrideViewPreference(.unspecified) + .preferredColorScheme(.none) + .prefersHomeIndicatorAutoHidden(false) } } diff --git a/JellyfinPlayer/NextUpView.swift b/JellyfinPlayer/NextUpView.swift index e32242c6..d73065ca 100644 --- a/JellyfinPlayer/NextUpView.swift +++ b/JellyfinPlayer/NextUpView.swift @@ -39,7 +39,7 @@ struct NextUpView: View { let json = try JSON(data: body) for (_,item):(String, JSON) in json["Items"] { // Do something you want - let itemObj = ResumeItem() + var itemObj = ResumeItem() itemObj.Image = item["SeriesPrimaryImageTag"].string ?? "" itemObj.ImageType = "Primary" itemObj.BlurHash = item["ImageBlurHashes"]["Primary"][itemObj.Image].string ?? "" diff --git a/JellyfinPlayer/SeasonItemView.swift b/JellyfinPlayer/SeasonItemView.swift index 59dbeaa7..d5abc0df 100644 --- a/JellyfinPlayer/SeasonItemView.swift +++ b/JellyfinPlayer/SeasonItemView.swift @@ -113,7 +113,7 @@ struct SeasonItemView: View { episode.ParentId = episode.SeasonId ?? ""; episode.CommunityRating = String(json["CommunityRating"].float ?? 0.0) - let rI = ResumeItem() + var rI = ResumeItem() rI.Name = episode.Name; rI.Id = episode.Id; rI.IndexNumber = episode.IndexNumber; diff --git a/JellyfinPlayer/SeriesItemView.swift b/JellyfinPlayer/SeriesItemView.swift index 037eeddf..279959ab 100644 --- a/JellyfinPlayer/SeriesItemView.swift +++ b/JellyfinPlayer/SeriesItemView.swift @@ -37,7 +37,7 @@ struct SeriesItemView: View { let json = try JSON(data: body) for (_,item):(String, JSON) in json["Items"] { // Do something you want - let itemObj = ResumeItem() + var itemObj = ResumeItem() itemObj.Type = "Season" itemObj.Id = item["Id"].string ?? "" itemObj.ProductionYear = item["ProductionYear"].int ?? 0 diff --git a/JellyfinPlayer/Views/SettingsView.swift b/JellyfinPlayer/Views/SettingsView.swift index 4b520ece..a618524c 100644 --- a/JellyfinPlayer/Views/SettingsView.swift +++ b/JellyfinPlayer/Views/SettingsView.swift @@ -101,17 +101,14 @@ struct SettingsView: View { } .navigationBarTitle("Settings", displayMode: .inline) - .toolbar { - ToolbarItemGroup(placement: .navigationBarLeading) { - Button { - close = false - } label: { - HStack() { - Text("Back").font(.callout) - } - } - } - } + .navigationBarItems(leading: + Button { + close = false + } label: { + HStack() { + Text("Back").font(.callout) + } + }) }.onAppear(perform: onAppear) } }