diff --git a/JellyfinPlayer/ConnectToServerView.swift b/JellyfinPlayer/ConnectToServerView.swift index 86b8ec29..03e0efd3 100644 --- a/JellyfinPlayer/ConnectToServerView.swift +++ b/JellyfinPlayer/ConnectToServerView.swift @@ -11,6 +11,7 @@ import SwiftyRequest import SwiftyJSON import CoreData import KeychainSwift +import Introspect struct ConnectToServerView: View { @Environment(\.managedObjectContext) private var viewContext @@ -193,5 +194,8 @@ struct ConnectToServerView: View { } .onAppear(perform: start) .transition(.move(edge:.bottom)) + .introspectTabBarController { (UITabBarController) in + UITabBarController.tabBar.isHidden = true + } } } diff --git a/JellyfinPlayer/ContentView.swift b/JellyfinPlayer/ContentView.swift index e331670e..a458657c 100644 --- a/JellyfinPlayer/ContentView.swift +++ b/JellyfinPlayer/ContentView.swift @@ -9,6 +9,7 @@ import SwiftUI import KeychainSwift import SwiftyRequest import SwiftyJSON +import Introspect class GlobalData: ObservableObject { @Published var user: SignedInUser? @@ -134,9 +135,6 @@ class PreferenceUIHostingController: UIHostingController { if(_orientations == .landscapeRight) { let value = UIInterfaceOrientation.landscapeRight.rawValue; UIDevice.current.setValue(value, forKey: "orientation") - } else { - let value = UIInterfaceOrientation.portrait.rawValue; - UIDevice.current.setValue(value, forKey: "orientation") } } }; @@ -186,6 +184,7 @@ struct ContentView: View { @State private var library_names: [String: String] = [:]; @State private var librariesShowRecentlyAdded: [String] = []; @State private var libraryPrefillID: String = ""; + @State private var showSettingsPopover: Bool = false; @Environment(\.verticalSizeClass) var verticalSizeClass: UserInterfaceSizeClass? @Environment(\.horizontalSizeClass) var horizontalSizeClass: UserInterfaceSizeClass? @@ -309,12 +308,12 @@ struct ContentView: View { .toolbar { ToolbarItemGroup(placement: .navigationBarTrailing) { Button { - print("Settings tapped!") + showSettingsPopover = true; } label: { Image(systemName: "gear") } } - } + }.popover( isPresented: self.$showSettingsPopover, arrowEdge: .bottom) { SettingsView(close: $showSettingsPopover).environmentObject(self.globalData) } } .tabItem({ Text("Home") @@ -337,14 +336,15 @@ struct ContentView: View { .navigationViewStyle(StackNavigationViewStyle()) .alert(isPresented: $isNetworkErrored) { Alert(title: Text("Network Error"), message: Text("Couldn't connect to Jellyfin"), dismissButton: .default(Text("Ok"))) + }.introspectTabBarController { (UITabBarController) in + UITabBarController.tabBar.isHidden = false } } else { Text("Signing in...") .onAppear(perform: { DispatchQueue.global(qos: .userInitiated).async { [self] in - print("Signing in") - sleep(3) - jsi.did = false + usleep(500000); + self.jsi.did = false; } }) } diff --git a/JellyfinPlayer/ContinueWatchingView.swift b/JellyfinPlayer/ContinueWatchingView.swift index a35abd09..289ad14e 100644 --- a/JellyfinPlayer/ContinueWatchingView.swift +++ b/JellyfinPlayer/ContinueWatchingView.swift @@ -75,78 +75,84 @@ struct ContinueWatchingView: View { } var body: some View { - VStack(alignment: .leading) { - ScrollView(.horizontal, showsIndicators: false) { - HStack() { - if(isLoading == false) { - Spacer().frame(width:16) - ForEach(resumeItems, id: \.Id) { item in - NavigationLink(destination: ItemView(item: item)) { - VStack(alignment: .leading) { - Spacer().frame(height: 10) - if(item.Type == "Episode") { - WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?fillWidth=560&fillHeight=315&quality=90&tag=\(item.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: (item.BlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item.BlurHash), size: CGSize(width: 32, height: 32))!) - .resizable() - .scaledToFit() - .cornerRadius(10) - } - .frame(width: 320, height: 180) - .cornerRadius(10) - .overlay( - ZStack { - Text("S\(String(item.ParentIndexNumber ?? 0)):E\(String(item.IndexNumber ?? 0)) - \(item.Name)") - .font(.caption) - .padding(6) - .foregroundColor(.white) - }.background(Color.black) - .opacity(0.8) - .cornerRadius(10.0) - .padding(6), alignment: .topTrailing - ) - .overlay( - RoundedRectangle(cornerRadius: 10, style: .circular) - .fill(Color(red: 172/255, green: 92/255, blue: 195/255).opacity(0.4)) - .frame(width: CGFloat((item.ItemProgress/100)*320), height: 180) - .padding(0), alignment: .bottomLeading - ) - .shadow(radius: 5) - } else { - WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?fillWidth=560&fillHeight=315&quality=90&tag=\(item.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: (item.BlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item.BlurHash), size: CGSize(width: 32, height: 32))!) - .resizable() - .scaledToFit() - .cornerRadius(10) - } - .frame(width: 320, height: 180) - .cornerRadius(10) - .overlay( - RoundedRectangle(cornerRadius: 10, style: .circular) - .fill(Color(red: 172/255, green: 92/255, blue: 195/255).opacity(0.4)) - .frame(width: CGFloat((item.ItemProgress/100)*320), height: 180) - .padding(0), alignment: .bottomLeading - ) - .shadow(radius: 5) - } - Text("\(item.Type == "Episode" ? item.SeriesName ?? "" : item.Name)") - .font(.callout) - .fontWeight(.semibold) - .foregroundColor(.primary) - Spacer().frame(height: 5) - }.padding(.trailing, 5) - } + if(resumeItems.count != 0) { + VStack(alignment: .leading) { + ScrollView(.horizontal, showsIndicators: false) { + HStack() { + if(isLoading == false) { + Spacer().frame(width:16) + ForEach(resumeItems, id: \.Id) { item in + NavigationLink(destination: ItemView(item: item)) { + VStack(alignment: .leading) { + Spacer().frame(height: 10) + if(item.Type == "Episode") { + WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?fillWidth=560&fillHeight=315&quality=90&tag=\(item.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: (item.BlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item.BlurHash), size: CGSize(width: 32, height: 32))!) + .resizable() + .scaledToFit() + .cornerRadius(10) + } + .frame(width: 320, height: 180) + .cornerRadius(10) + .overlay( + ZStack { + Text("S\(String(item.ParentIndexNumber ?? 0)):E\(String(item.IndexNumber ?? 0)) - \(item.Name)") + .font(.caption) + .padding(6) + .foregroundColor(.white) + }.background(Color.black) + .opacity(0.8) + .cornerRadius(10.0) + .padding(6), alignment: .topTrailing + ) + .overlay( + RoundedRectangle(cornerRadius: 10, style: .circular) + .fill(Color(red: 172/255, green: 92/255, blue: 195/255).opacity(0.4)) + .frame(width: CGFloat((item.ItemProgress/100)*320), height: 180) + .padding(0), alignment: .bottomLeading + ) + .shadow(radius: 5) + } else { + WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?fillWidth=560&fillHeight=315&quality=90&tag=\(item.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: (item.BlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item.BlurHash), size: CGSize(width: 32, height: 32))!) + .resizable() + .scaledToFit() + .cornerRadius(10) + } + .frame(width: 320, height: 180) + .cornerRadius(10) + .overlay( + RoundedRectangle(cornerRadius: 10, style: .circular) + .fill(Color(red: 172/255, green: 92/255, blue: 195/255).opacity(0.4)) + .frame(width: CGFloat((item.ItemProgress/100)*320), height: 180) + .padding(0), alignment: .bottomLeading + ) + .shadow(radius: 5) + } + Text("\(item.Type == "Episode" ? item.SeriesName ?? "" : item.Name)") + .font(.callout) + .fontWeight(.semibold) + .foregroundColor(.primary) + Spacer().frame(height: 5) + }.padding(.trailing, 5) + } + } + Spacer().frame(width:14) } - Spacer().frame(width:14) } } + .frame(height: 200) + .padding(.bottom, 10) } - .frame(height: 200) - .padding(.bottom, 10) - }.onAppear(perform: onAppear) + } else { + VStack() { + EmptyView() + }.onAppear(perform: onAppear) + } } } diff --git a/JellyfinPlayer/MovieItemView.swift b/JellyfinPlayer/MovieItemView.swift index 3e59be2c..a19cb931 100644 --- a/JellyfinPlayer/MovieItemView.swift +++ b/JellyfinPlayer/MovieItemView.swift @@ -240,6 +240,14 @@ struct MovieItemView: View { } } + @Environment(\.verticalSizeClass) var verticalSizeClass: UserInterfaceSizeClass? + @Environment(\.horizontalSizeClass) var horizontalSizeClass: UserInterfaceSizeClass? + + var isPortrait: Bool { + let result = verticalSizeClass == .regular && horizontalSizeClass == .compact + return result + } + var body: some View { if(playing) { PlayerDemo(item: fullItem, playing: $playing).onAppear(perform: lockOrientations) @@ -247,191 +255,375 @@ struct MovieItemView: View { LoadingView(isShowing: $isLoading) { VStack(alignment:.leading) { if(!isLoading) { - GeometryReader { geometry in - VStack() { - WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.Id)/Images/Backdrop?maxWidth=3840&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))!) - .resizable() - .frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing, height: (geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing) * 0.5625) - } - - .opacity(0.4) - .frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing, height: (geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing) * 0.5625) - .aspectRatio(contentMode: .fill) - .shadow(radius: 5) - .overlay( + if(isPortrait) { + GeometryReader { geometry in + VStack() { + WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.Id)/Images/Backdrop?maxWidth=3840&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))!) + .resizable() + .frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing, height: (geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing) * 0.5625) + } + + .opacity(0.4) + .aspectRatio(contentMode: .fill) + .frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing, height: (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?fillWidth=300&fillHeight=450&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) + .padding(EdgeInsets(top: 1, leading: 4, bottom: 1, trailing: 4)) + .overlay( + RoundedRectangle(cornerRadius: 2) + .stroke(Color.secondary, lineWidth: 1) + ) + } + if(fullItem.CommunityRating != "") { + HStack() { + Image(systemName: "star").foregroundColor(.secondary) + Text(fullItem.CommunityRating).font(.subheadline) + .fontWeight(.semibold) + .foregroundColor(.secondary) + .lineLimit(1) + .offset(x: -7, y: 0.7) + } + } + } + + }.offset(x: 0, y: -46) + }.offset(x: 16, y: 40) + , alignment: .bottomLeading) + VStack(alignment: .leading) { HStack() { - WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.Id)/Images/Primary?fillWidth=300&fillHeight=450&quality=90&tag=\(fullItem.Poster)")!) + //Play button + Button() { + playing = true; + } label: { + HStack() { + Text(fullItem.Progress == 0 ? "Play" : "\(progressString) left").foregroundColor(Color.white).font(.callout).fontWeight(.semibold) + Image(systemName: "play.fill").foregroundColor(Color.white).font(.system(size: 20)) + } + .frame(width: 120, height: 35) + .background(Color(UIColor.systemBlue)) + .cornerRadius(10) + }.buttonStyle(PlainButtonStyle()) + .frame(width: 120, height: 25) + Spacer() + HStack() { + Button() { + favorite.toggle() + } label: { + if(!favorite) { + Image(systemName: "heart").foregroundColor(Color.primary).font(.system(size: 20)) + } else { + Image(systemName: "heart.fill").foregroundColor(Color(UIColor.systemRed)).font(.system(size: 20)) + } + } + Button() { + watched.toggle() + } label: { + if(watched) { + Image(systemName: "checkmark.rectangle.fill").foregroundColor(Color.primary).font(.system(size: 20)) + } else { + Image(systemName: "xmark.rectangle").foregroundColor(Color.primary).font(.system(size: 20)) + } + } + } + }.padding(.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) + } + 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) { + ScrollView(.horizontal, showsIndicators: false) { + 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)) { + Text(genre.Name).font(.footnote) + } + } + }.padding(.leading, 16).padding(.trailing,16) + } + } + if(fullItem.Cast.count != 0) { + ScrollView(.horizontal, showsIndicators: false) { + 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() { + 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: 32, height: 32))!) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: 100, height: 100) + .cornerRadius(10) + } + .aspectRatio(contentMode: .fill) + .frame(width: 100, height: 100) + .cornerRadius(10).shadow(radius: 6) + 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) + } + } + }.padding(.top, -3) + } + if(fullItem.Directors.count != 0) { + HStack() { + Text("Directors:").font(.callout).fontWeight(.semibold) + Text(fullItem.Directors.joined(separator: ", ")).font(.footnote).lineLimit(1).foregroundColor(Color.secondary) + }.padding(.leading, 16).padding(.trailing,16) + } + if(fullItem.Writers.count != 0) { + HStack() { + Text("Writers:").font(.callout).fontWeight(.semibold) + Text(fullItem.Writers.joined(separator: ", ")).font(.footnote).lineLimit(1).foregroundColor(Color.secondary) + }.padding(.leading, 16).padding(.trailing,16) + } + if(fullItem.Studios.count != 0) { + HStack() { + Text("Studios:").font(.callout).fontWeight(.semibold) + Text(fullItem.Studios.joined(separator: ", ")).font(.footnote).lineLimit(1).foregroundColor(Color.secondary) + }.padding(.leading, 16).padding(.trailing,16) + } + Spacer().frame(height: 3) + } + } + }.padding(EdgeInsets(top: 24, leading: 0, bottom: 0, trailing: 0)) + } + } + } else { + GeometryReader { geometry in + ZStack() { + WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.Id)/Images/Backdrop?maxWidth=3840&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))!) + .resizable() + .frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing, height: (geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing) * 0.5625) + } + + .opacity(0.4) + .aspectRatio(contentMode: .fill) + .frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing, height: (geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing) * 0.5625) + .edgesIgnoringSafeArea(.all) + HStack() { + VStack() { + WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.Id)/Images/Primary?maxWidth=300&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) + .shadow(radius: 5) + Spacer().frame(height: 15) + Button() { + playing = true; + } label: { 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) + 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(UIColor.systemBlue)) + .cornerRadius(10) + }.buttonStyle(PlainButtonStyle()) + .frame(width: 120, height: 25) + Spacer() + } + ScrollView() { + VStack(alignment: .leading) { + HStack() { + VStack(alignment: .leading) { + Text(fullItem.Name).font(.headline) .fontWeight(.semibold) - .foregroundColor(.secondary) - .lineLimit(1) - .padding(EdgeInsets(top: 1, leading: 4, bottom: 1, trailing: 4)) - .overlay( - RoundedRectangle(cornerRadius: 2) - .stroke(Color.secondary, lineWidth: 1) - ) - } - if(fullItem.CommunityRating != "") { + .foregroundColor(.primary) + .fixedSize(horizontal: false, vertical: true) + .offset(x: 11, y: 0) + Spacer().frame(height: 1) HStack() { - Image(systemName: "star").foregroundColor(.secondary) - Text(fullItem.CommunityRating).font(.subheadline) - .fontWeight(.semibold) + Text(String(fullItem.ProductionYear)).font(.subheadline) + .fontWeight(.medium) .foregroundColor(.secondary) .lineLimit(1) - .offset(x: -7, y: 0.7) + Text(fullItem.Runtime).font(.subheadline) + .fontWeight(.medium) + .foregroundColor(.secondary) + .lineLimit(1) + if(fullItem.OfficialRating != "") { + Text(fullItem.OfficialRating).font(.subheadline) + .fontWeight(.semibold) + .foregroundColor(.secondary) + .lineLimit(1) + .padding(EdgeInsets(top: 1, leading: 4, bottom: 1, trailing: 4)) + .overlay( + RoundedRectangle(cornerRadius: 2) + .stroke(Color.secondary, lineWidth: 1) + ) + } + if(fullItem.CommunityRating != "") { + HStack() { + Image(systemName: "star").foregroundColor(.secondary) + Text(fullItem.CommunityRating).font(.subheadline) + .fontWeight(.semibold) + .foregroundColor(.secondary) + .lineLimit(1) + .offset(x: -7, y: 0.7) + } + } + Spacer() + }.frame(maxWidth: .infinity) + .offset(x: 11) + }.frame(maxWidth: .infinity) + Spacer() + HStack() { + Button() { + favorite.toggle() + } label: { + if(!favorite) { + Image(systemName: "heart").foregroundColor(Color.primary).font(.system(size: 20)) + } else { + Image(systemName: "heart.fill").foregroundColor(Color(UIColor.systemRed)).font(.system(size: 20)) + } + } + Button() { + watched.toggle() + } label: { + if(watched) { + Image(systemName: "checkmark.rectangle.fill").foregroundColor(Color.primary).font(.system(size: 20)) + } else { + Image(systemName: "xmark.rectangle").foregroundColor(Color.primary).font(.system(size: 20)) + } } } } - - }.offset(x: 0, y: -46) - }.offset(x: 16, y: 40) - , alignment: .bottomLeading) - VStack(alignment: .leading) { - HStack() { - //Play button - Button() { - playing = true; - } label: { - HStack() { - Text(fullItem.Progress == 0 ? "Play" : "\(progressString) left").foregroundColor(Color.white).font(.callout).fontWeight(.semibold) - Image(systemName: "play.fill").foregroundColor(Color.white).font(.system(size: 20)) - } - .frame(width: 120, height: 35) - .background(Color(UIColor.systemBlue)) - .cornerRadius(10) - }.buttonStyle(PlainButtonStyle()) - .frame(width: 120, height: 25) - Spacer() - HStack() { - Button() { - favorite.toggle() - } label: { - if(!favorite) { - Image(systemName: "heart").foregroundColor(Color.primary).font(.system(size: 20)) - } else { - Image(systemName: "heart.fill").foregroundColor(Color(UIColor.systemRed)).font(.system(size: 20)) + if(fullItem.Tagline != "") { + Text(fullItem.Tagline).font(.body).italic().padding(.top, 3).fixedSize(horizontal: false, vertical: true).padding(.leading, 16).padding(.trailing,16) } - } - Button() { - watched.toggle() - } label: { - if(watched) { - Image(systemName: "checkmark.rectangle.fill").foregroundColor(Color.primary).font(.system(size: 20)) - } else { - Image(systemName: "xmark.rectangle").foregroundColor(Color.primary).font(.system(size: 20)) - } - } - } - }.padding(.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) - } - 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) { - ScrollView(.horizontal, showsIndicators: false) { - 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)) { - Text(genre.Name).font(.footnote) - } - } - }.padding(.leading, 16).padding(.trailing,16) - } - } - if(fullItem.Cast.count != 0) { - ScrollView(.horizontal, showsIndicators: false) { - VStack() { - Spacer().frame(height: 8); + 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) { + ScrollView(.horizontal, showsIndicators: false) { HStack() { - Spacer().frame(width: 16) - ForEach(fullItem.Cast, id: \.Id) { cast in - NavigationLink(destination: LibraryView(extraParams: "&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: 32, height: 32))!) - .resizable() - .aspectRatio(contentMode: .fill) - .frame(width: 100, height: 100) - .cornerRadius(10) + 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)) { + Text(genre.Name).font(.footnote) + } + } + }.padding(.leading, 16).padding(.trailing,16) + } + } + if(fullItem.Cast.count != 0) { + ScrollView(.horizontal, showsIndicators: false) { + 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() { + 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: 32, height: 32))!) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: 100, height: 100) + .cornerRadius(10) + } + .aspectRatio(contentMode: .fill) + .frame(width: 100, height: 100) + .cornerRadius(10).shadow(radius: 6) + 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) } - .aspectRatio(contentMode: .fill) - .frame(width: 100, height: 100) - .cornerRadius(10).shadow(radius: 6) - 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: 10) + Spacer().frame(width: 16) } - Spacer().frame(width: 16) } - } - }.padding(.top, -3) - } - if(fullItem.Directors.count != 0) { - HStack() { - Text("Directors:").font(.callout).fontWeight(.semibold) - Text(fullItem.Directors.joined(separator: ", ")).font(.footnote).lineLimit(1).foregroundColor(Color.secondary) - }.padding(.leading, 16).padding(.trailing,16) - } - if(fullItem.Writers.count != 0) { - HStack() { - Text("Writers:").font(.callout).fontWeight(.semibold) - Text(fullItem.Writers.joined(separator: ", ")).font(.footnote).lineLimit(1).foregroundColor(Color.secondary) - }.padding(.leading, 16).padding(.trailing,16) - } - if(fullItem.Studios.count != 0) { - HStack() { - Text("Studios:").font(.callout).fontWeight(.semibold) - Text(fullItem.Studios.joined(separator: ", ")).font(.footnote).lineLimit(1).foregroundColor(Color.secondary) - }.padding(.leading, 16).padding(.trailing,16) - } - Spacer().frame(height: 3) - } - } - }.padding(EdgeInsets(top: 24, leading: 0, bottom: 0, trailing: 0)) + }.padding(.top, -3) + } + if(fullItem.Directors.count != 0) { + HStack() { + Text("Directors:").font(.callout).fontWeight(.semibold) + Text(fullItem.Directors.joined(separator: ", ")).font(.footnote).lineLimit(1).foregroundColor(Color.secondary) + }.padding(.leading, 16).padding(.trailing,16) + } + if(fullItem.Writers.count != 0) { + HStack() { + Text("Writers:").font(.callout).fontWeight(.semibold) + Text(fullItem.Writers.joined(separator: ", ")).font(.footnote).lineLimit(1).foregroundColor(Color.secondary) + }.padding(.leading, 16).padding(.trailing,16) + } + if(fullItem.Studios.count != 0) { + HStack() { + Text("Studios:").font(.callout).fontWeight(.semibold) + Text(fullItem.Studios.joined(separator: ", ")).font(.footnote).lineLimit(1).foregroundColor(Color.secondary) + }.padding(.leading, 16).padding(.trailing,16) + } + Spacer().frame(height: 195); + }.frame(maxHeight: .infinity) + }.padding(.trailing, 55) + }.padding(.top, 12) + } } } } } .navigationBarTitleDisplayMode(.inline) - .navigationTitle("Movie Details") + .navigationTitle(fullItem.Name) .supportedOrientations(.allButUpsideDown) .prefersHomeIndicatorAutoHidden(false) .withHostingWindow() { window in diff --git a/JellyfinPlayer/NextUpView.swift b/JellyfinPlayer/NextUpView.swift index 37ce96d0..fc5a11ce 100644 --- a/JellyfinPlayer/NextUpView.swift +++ b/JellyfinPlayer/NextUpView.swift @@ -69,46 +69,48 @@ struct NextUpView: View { var body: some View { VStack(alignment: .leading) { - Text("Next Up").font(.title2).fontWeight(.bold).padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16)) - ScrollView(.horizontal, showsIndicators: false) { - HStack() { - if(isLoading == false) { - Spacer().frame(width:18) - ForEach(resumeItems, id: \.Id) { item in - NavigationLink(destination: ItemView(item: item)) { - VStack(alignment: .leading) { - Spacer().frame(height:10) - WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.SeriesId ?? "")/Images/\(item.ImageType)?fillWidth=300&fillHeight=450&quality=90&tag=\(item.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: (item.BlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item.BlurHash), size: CGSize(width: 32, height: 32))!) - .resizable() - .scaledToFit() - .cornerRadius(10) - } - .frame(width: 100, height: 150) - .cornerRadius(10) - .shadow(radius: 6) - Text(item.SeriesName ?? "") - .font(.caption) - .fontWeight(.semibold) - .foregroundColor(.primary) - .lineLimit(1) - Text("S\(String(item.ParentIndexNumber ?? 0)):E\(String(item.IndexNumber ?? 0))") - .font(.caption) - .fontWeight(.semibold) - .foregroundColor(.secondary) - .lineLimit(1) - Spacer().frame(height:5) + if(resumeItems.count != 0) { + Text("Next Up").font(.title2).fontWeight(.bold).padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16)) + ScrollView(.horizontal, showsIndicators: false) { + HStack() { + if(isLoading == false) { + Spacer().frame(width:18) + ForEach(resumeItems, id: \.Id) { item in + NavigationLink(destination: ItemView(item: item)) { + VStack(alignment: .leading) { + Spacer().frame(height:10) + WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.SeriesId ?? "")/Images/\(item.ImageType)?fillWidth=300&fillHeight=450&quality=90&tag=\(item.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: (item.BlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item.BlurHash), size: CGSize(width: 32, height: 32))!) + .resizable() + .scaledToFit() + .cornerRadius(10) + } + .frame(width: 100, height: 150) + .cornerRadius(10) + .shadow(radius: 6) + Text(item.SeriesName ?? "") + .font(.caption) + .fontWeight(.semibold) + .foregroundColor(.primary) + .lineLimit(1) + Text("S\(String(item.ParentIndexNumber ?? 0)):E\(String(item.IndexNumber ?? 0))") + .font(.caption) + .fontWeight(.semibold) + .foregroundColor(.secondary) + .lineLimit(1) + Spacer().frame(height:5) + } + .frame(width: 100) + Spacer().frame(width:12) } - .frame(width: 100) - Spacer().frame(width:12) } + Spacer().frame(width:18) } - Spacer().frame(width:18) } - } - }.padding(EdgeInsets(top: -2, leading: 0, bottom: 0, trailing: 0)) + }.padding(EdgeInsets(top: -2, leading: 0, bottom: 0, trailing: 0)) + } }.onAppear(perform: onAppear).padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) } } diff --git a/JellyfinPlayer/SettingsView.swift b/JellyfinPlayer/SettingsView.swift index b417dd89..69473b04 100644 --- a/JellyfinPlayer/SettingsView.swift +++ b/JellyfinPlayer/SettingsView.swift @@ -8,13 +8,22 @@ import SwiftUI struct SettingsView: View { + @Binding var close: Bool; var body: some View { - Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) - } -} - -struct SettingsView_Previews: PreviewProvider { - static var previews: some View { - SettingsView() + NavigationView() { + Text("SettingsView not implemented.") + .navigationBarTitle("Settings", displayMode: .inline) + .toolbar { + ToolbarItemGroup(placement: .navigationBarLeading) { + Button { + close = false + } label: { + HStack() { + Text("Back").font(.callout) + } + } + } + } + } } }