Add movie item image description

This commit is contained in:
Aiden Vigue 2021-05-15 21:27:09 -04:00
parent e878e4539c
commit 437b71960b
7 changed files with 499 additions and 254 deletions

View File

@ -12,7 +12,6 @@
5338F754263B65E10014BF09 /* SwiftyRequest in Frameworks */ = {isa = PBXBuildFile; productRef = 5338F753263B65E10014BF09 /* SwiftyRequest */; }; 5338F754263B65E10014BF09 /* SwiftyRequest in Frameworks */ = {isa = PBXBuildFile; productRef = 5338F753263B65E10014BF09 /* SwiftyRequest */; };
5338F757263B7E2E0014BF09 /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 5338F756263B7E2E0014BF09 /* KeychainSwift */; }; 5338F757263B7E2E0014BF09 /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 5338F756263B7E2E0014BF09 /* KeychainSwift */; };
535BAE9F2649E569005FA86D /* ItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535BAE9E2649E569005FA86D /* ItemView.swift */; }; 535BAE9F2649E569005FA86D /* ItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535BAE9E2649E569005FA86D /* ItemView.swift */; };
535BAEA32649E96A005FA86D /* JellyfinHLSResourceLoaderDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535BAEA22649E96A005FA86D /* JellyfinHLSResourceLoaderDelegate.swift */; };
535BAEA5264A151C005FA86D /* VLCPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535BAEA4264A151C005FA86D /* VLCPlayer.swift */; }; 535BAEA5264A151C005FA86D /* VLCPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535BAEA4264A151C005FA86D /* VLCPlayer.swift */; };
535BAEA7264A18AA005FA86D /* PlayerDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535BAEA6264A18AA005FA86D /* PlayerDemo.swift */; }; 535BAEA7264A18AA005FA86D /* PlayerDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535BAEA6264A18AA005FA86D /* PlayerDemo.swift */; };
5377CBF5263B596A003A4E83 /* JellyfinPlayerApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5377CBF4263B596A003A4E83 /* JellyfinPlayerApp.swift */; }; 5377CBF5263B596A003A4E83 /* JellyfinPlayerApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5377CBF4263B596A003A4E83 /* JellyfinPlayerApp.swift */; };
@ -39,6 +38,7 @@
53E4E645263F6BC000F67C6B /* PartialSheet in Frameworks */ = {isa = PBXBuildFile; productRef = 53E4E644263F6BC000F67C6B /* PartialSheet */; }; 53E4E645263F6BC000F67C6B /* PartialSheet in Frameworks */ = {isa = PBXBuildFile; productRef = 53E4E644263F6BC000F67C6B /* PartialSheet */; };
53E4E647263F6CF100F67C6B /* LibraryFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53E4E646263F6CF100F67C6B /* LibraryFilterView.swift */; }; 53E4E647263F6CF100F67C6B /* LibraryFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53E4E646263F6CF100F67C6B /* LibraryFilterView.swift */; };
53E4E649263F725B00F67C6B /* MultiSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53E4E648263F725B00F67C6B /* MultiSelector.swift */; }; 53E4E649263F725B00F67C6B /* MultiSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53E4E648263F725B00F67C6B /* MultiSelector.swift */; };
53EE24E6265060780068F029 /* LibrarySearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53EE24E5265060780068F029 /* LibrarySearchView.swift */; };
53FF7F2A263CF3F500585C35 /* LatestMediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53FF7F29263CF3F500585C35 /* LatestMediaView.swift */; }; 53FF7F2A263CF3F500585C35 /* LatestMediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53FF7F29263CF3F500585C35 /* LatestMediaView.swift */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
@ -59,7 +59,6 @@
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
5338F74D263B61370014BF09 /* ConnectToServerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectToServerView.swift; sourceTree = "<group>"; }; 5338F74D263B61370014BF09 /* ConnectToServerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectToServerView.swift; sourceTree = "<group>"; };
535BAE9E2649E569005FA86D /* ItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemView.swift; sourceTree = "<group>"; }; 535BAE9E2649E569005FA86D /* ItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemView.swift; sourceTree = "<group>"; };
535BAEA22649E96A005FA86D /* JellyfinHLSResourceLoaderDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinHLSResourceLoaderDelegate.swift; sourceTree = "<group>"; };
535BAEA4264A151C005FA86D /* VLCPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VLCPlayer.swift; sourceTree = "<group>"; }; 535BAEA4264A151C005FA86D /* VLCPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VLCPlayer.swift; sourceTree = "<group>"; };
535BAEA6264A18AA005FA86D /* PlayerDemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerDemo.swift; sourceTree = "<group>"; }; 535BAEA6264A18AA005FA86D /* PlayerDemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerDemo.swift; sourceTree = "<group>"; };
5377CBF1263B596A003A4E83 /* JellyfinPlayer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = JellyfinPlayer.app; sourceTree = BUILT_PRODUCTS_DIR; }; 5377CBF1263B596A003A4E83 /* JellyfinPlayer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = JellyfinPlayer.app; sourceTree = BUILT_PRODUCTS_DIR; };
@ -82,6 +81,7 @@
53DF641D263D9C0600A7CD1A /* LibraryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryView.swift; sourceTree = "<group>"; }; 53DF641D263D9C0600A7CD1A /* LibraryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryView.swift; sourceTree = "<group>"; };
53E4E646263F6CF100F67C6B /* LibraryFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryFilterView.swift; sourceTree = "<group>"; }; 53E4E646263F6CF100F67C6B /* LibraryFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryFilterView.swift; sourceTree = "<group>"; };
53E4E648263F725B00F67C6B /* MultiSelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiSelector.swift; sourceTree = "<group>"; }; 53E4E648263F725B00F67C6B /* MultiSelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiSelector.swift; sourceTree = "<group>"; };
53EE24E5265060780068F029 /* LibrarySearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibrarySearchView.swift; sourceTree = "<group>"; };
53FF7F29263CF3F500585C35 /* LatestMediaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LatestMediaView.swift; sourceTree = "<group>"; }; 53FF7F29263CF3F500585C35 /* LatestMediaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LatestMediaView.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
@ -147,9 +147,9 @@
53E4E648263F725B00F67C6B /* MultiSelector.swift */, 53E4E648263F725B00F67C6B /* MultiSelector.swift */,
535BAE9E2649E569005FA86D /* ItemView.swift */, 535BAE9E2649E569005FA86D /* ItemView.swift */,
53A089CF264DA9DA00D57806 /* MovieItemView.swift */, 53A089CF264DA9DA00D57806 /* MovieItemView.swift */,
535BAEA22649E96A005FA86D /* JellyfinHLSResourceLoaderDelegate.swift */,
535BAEA4264A151C005FA86D /* VLCPlayer.swift */, 535BAEA4264A151C005FA86D /* VLCPlayer.swift */,
535BAEA6264A18AA005FA86D /* PlayerDemo.swift */, 535BAEA6264A18AA005FA86D /* PlayerDemo.swift */,
53EE24E5265060780068F029 /* LibrarySearchView.swift */,
); );
path = JellyfinPlayer; path = JellyfinPlayer;
sourceTree = "<group>"; sourceTree = "<group>";
@ -278,9 +278,9 @@
5377CBF7263B596A003A4E83 /* ContentView.swift in Sources */, 5377CBF7263B596A003A4E83 /* ContentView.swift in Sources */,
5389277C263CC3DB0035E14B /* BlurHashDecode.swift in Sources */, 5389277C263CC3DB0035E14B /* BlurHashDecode.swift in Sources */,
539B2DA5263BA5B8007FF1A4 /* SettingsView.swift in Sources */, 539B2DA5263BA5B8007FF1A4 /* SettingsView.swift in Sources */,
535BAEA32649E96A005FA86D /* JellyfinHLSResourceLoaderDelegate.swift in Sources */,
5338F74E263B61370014BF09 /* ConnectToServerView.swift in Sources */, 5338F74E263B61370014BF09 /* ConnectToServerView.swift in Sources */,
5377CBF5263B596A003A4E83 /* JellyfinPlayerApp.swift in Sources */, 5377CBF5263B596A003A4E83 /* JellyfinPlayerApp.swift in Sources */,
53EE24E6265060780068F029 /* LibrarySearchView.swift in Sources */,
53892772263C8C6F0035E14B /* LoadingView.swift in Sources */, 53892772263C8C6F0035E14B /* LoadingView.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;

View File

@ -96,8 +96,8 @@
"repositoryURL": "https://github.com/apple/swift-nio.git", "repositoryURL": "https://github.com/apple/swift-nio.git",
"state": { "state": {
"branch": null, "branch": null,
"revision": "21782f3bdc9581148d38d0ccaab6ec952ccda56b", "revision": "d161bf658780b209c185994528e7e24376cf7283",
"version": "2.28.0" "version": "2.29.0"
} }
}, },
{ {
@ -114,8 +114,8 @@
"repositoryURL": "https://github.com/apple/swift-nio-ssl.git", "repositoryURL": "https://github.com/apple/swift-nio-ssl.git",
"state": { "state": {
"branch": null, "branch": null,
"revision": "3d576964a1ace80d2a3f8bab96cab03e5ee074dc", "revision": "6363cdf6d2fb863e82434f3c4618f4e896e37569",
"version": "2.12.0" "version": "2.13.1"
} }
}, },
{ {

View File

@ -179,6 +179,7 @@ struct ContentView: View {
@State private var needsToSelectServer = false; @State private var needsToSelectServer = false;
@State private var isSignInErrored = false; @State private var isSignInErrored = false;
@State private var isNetworkErrored = false;
@State private var isLoading = false; @State private var isLoading = false;
@State private var tabSelection: String = "Home"; @State private var tabSelection: String = "Home";
@State private var libraries: [String] = []; @State private var libraries: [String] = [];
@ -256,9 +257,14 @@ struct ContentView: View {
} }
break break
case .failure( _): case .failure( let error):
_isLoading.wrappedValue = false; if(error.response?.status.code == 401) {
_isSignInErrored.wrappedValue = true; _isLoading.wrappedValue = false;
_isSignInErrored.wrappedValue = true;
} else {
_isLoading.wrappedValue = false;
_isNetworkErrored.wrappedValue = true;
}
} }
} }
} }
@ -323,10 +329,14 @@ struct ContentView: View {
Image(systemName: "folder") Image(systemName: "folder")
}) })
.tag("All Media") .tag("All Media")
} }
}.environmentObject(globalData) }.environmentObject(globalData)
.onAppear(perform: startup) .onAppear(perform: startup)
.navigationViewStyle(StackNavigationViewStyle()) .navigationViewStyle(StackNavigationViewStyle())
.alert(isPresented: $isNetworkErrored) {
Alert(title: Text("Network Error"), message: Text("Couldn't connect to Jellyfin"), dismissButton: .default(Text("Ok")))
}
} }
} }

View File

@ -1,148 +0,0 @@
//
// JellyfinHLSResourceLoaderDelegate.swift
import Foundation
import AVFoundation
class JellyfinHLSResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSessionDataDelegate, URLSessionTaskDelegate {
typealias Completion = (URL?) -> Void
private static let SchemeSuffix = "icpt"
// MARK: - Properties
// MARK: Public
var completion: Completion?
lazy var streamingAssetURL: URL = {
guard var components = URLComponents(url: self.url, resolvingAgainstBaseURL: false) else {
fatalError()
}
components.scheme = (components.scheme ?? "") + JellyfinHLSResourceLoaderDelegate.SchemeSuffix
guard let retURL = components.url else {
fatalError()
}
return retURL
}()
// MARK: Private
private let url: URL
private var infoResponse: URLResponse?
private var urlSession: URLSession?
private lazy var mediaData = Data()
private var loadingRequests = [AVAssetResourceLoadingRequest]()
// MARK: - Life Cycle Methods
init(withURL url: URL) {
self.url = url
super.init()
}
// MARK: - Public Methods
func invalidate() {
self.loadingRequests.forEach { $0.finishLoading() }
self.invalidateURLSession()
}
// MARK: - AVAssetResourceLoaderDelegate
func resourceLoader(_ resourceLoader: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool {
if self.urlSession == nil {
self.urlSession = self.createURLSession()
let task = self.urlSession!.dataTask(with: self.url)
task.resume()
}
self.loadingRequests.append(loadingRequest)
return true
}
func resourceLoader(_ resourceLoader: AVAssetResourceLoader, didCancel loadingRequest: AVAssetResourceLoadingRequest) {
if let index = self.loadingRequests.firstIndex(of: loadingRequest) {
self.loadingRequests.remove(at: index)
}
}
// MARK: - URLSessionDataDelegate
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
self.infoResponse = response
self.processRequests()
completionHandler(.allow)
}
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
self.mediaData.append(data)
self.processRequests()
}
// MARK: - Private Methods
private func createURLSession() -> URLSession {
let config = URLSessionConfiguration.default
let operationQueue = OperationQueue()
operationQueue.maxConcurrentOperationCount = 1
return URLSession(configuration: config, delegate: self, delegateQueue: operationQueue)
}
private func invalidateURLSession() {
self.urlSession?.invalidateAndCancel()
self.urlSession = nil
}
private func isInfo(request: AVAssetResourceLoadingRequest) -> Bool {
return request.contentInformationRequest != nil
}
private func fillInfoRequest(request: inout AVAssetResourceLoadingRequest, response: URLResponse) {
request.contentInformationRequest?.isByteRangeAccessSupported = true
request.contentInformationRequest?.contentType = response.mimeType
request.contentInformationRequest?.contentLength = response.expectedContentLength
}
private func processRequests() {
var finishedRequests = Set<AVAssetResourceLoadingRequest>()
self.loadingRequests.forEach {
var request = $0
if self.isInfo(request: request), let response = self.infoResponse {
self.fillInfoRequest(request: &request, response: response)
}
if let dataRequest = request.dataRequest, self.checkAndRespond(forRequest: dataRequest) {
finishedRequests.insert(request)
request.finishLoading()
}
}
self.loadingRequests = self.loadingRequests.filter { !finishedRequests.contains($0) }
}
private func checkAndRespond(forRequest dataRequest: AVAssetResourceLoadingDataRequest) -> Bool {
let downloadedData = self.mediaData
let downloadedDataLength = Int64(downloadedData.count)
let requestRequestedOffset = dataRequest.requestedOffset
let requestRequestedLength = Int64(dataRequest.requestedLength)
let requestCurrentOffset = dataRequest.currentOffset
if downloadedDataLength < requestCurrentOffset {
return false
}
let downloadedUnreadDataLength = downloadedDataLength - requestCurrentOffset
let requestUnreadDataLength = requestRequestedOffset + requestRequestedLength - requestCurrentOffset
let respondDataLength = min(requestUnreadDataLength, downloadedUnreadDataLength)
dataRequest.respond(with: downloadedData.subdata(in: Range(NSMakeRange(Int(requestCurrentOffset), Int(respondDataLength)))!))
let requestEndOffset = requestRequestedOffset + requestRequestedLength
return requestCurrentOffset >= requestEndOffset
}
}

View File

@ -0,0 +1,183 @@
//
// LibrarySearchView.swift
// JellyfinPlayer
//
// Created by Aiden Vigue on 5/2/21.
//
import SwiftUI
import SwiftyJSON
import SwiftyRequest
import ExyteGrid
import SDWebImageSwiftUI
struct LibrarySearchView: View {
@Environment(\.managedObjectContext) private var viewContext
@EnvironmentObject var globalData: GlobalData
@State var url: String;
@Binding var close: Bool;
@State var open: Bool = false;
@State private var isLoading: Bool = true;
@State private var onlyUnplayed: Bool = false;
@State private var viewDidLoad: Bool = false;
@State var items: [ResumeItem] = []
@State var linkedItem: ResumeItem = ResumeItem();
@State var searchQuery: String = "" {
didSet {
self.onAppear();
}
};
func onAppear() {
_isLoading.wrappedValue = true;
_items.wrappedValue = [];
let request = RestRequest(method: .get, url: (globalData.server?.baseURI ?? "") + _url.wrappedValue + "&searchTerm=" + searchQuery)
request.headerParameters["X-Emby-Authorization"] = globalData.authHeader
request.contentType = "application/json"
request.acceptType = "application/json"
request.responseData() { (result: Result<RestResponse<Data>, RestError>) in
switch result {
case .success(let response):
let body = response.body
do {
let json = try JSON(data: body)
for (_,item):(String, JSON) in json["Items"] {
// Do something you want
let itemObj = ResumeItem()
itemObj.Type = item["Type"].string ?? ""
if(itemObj.Type == "Series") {
itemObj.ItemBadge = item["UserData"]["UnplayedItemCount"].int ?? 0
itemObj.Image = item["ImageTags"]["Primary"].string ?? ""
itemObj.ImageType = "Primary"
itemObj.BlurHash = item["ImageBlurHashes"]["Primary"][itemObj.Image].string ?? ""
itemObj.Name = item["Name"].string ?? ""
itemObj.Type = item["Type"].string ?? ""
itemObj.IndexNumber = nil
itemObj.Id = item["Id"].string ?? ""
itemObj.ParentIndexNumber = nil
itemObj.SeasonId = nil
itemObj.SeriesId = nil
itemObj.SeriesName = nil
itemObj.ProductionYear = item["ProductionYear"].int ?? 0
} else {
itemObj.ProductionYear = item["ProductionYear"].int ?? 0
itemObj.Image = item["ImageTags"]["Primary"].string ?? ""
itemObj.ImageType = "Primary"
itemObj.BlurHash = item["ImageBlurHashes"]["Primary"][itemObj.Image].string ?? ""
itemObj.Name = item["Name"].string ?? ""
itemObj.Type = item["Type"].string ?? ""
itemObj.IndexNumber = item["IndexNumber"].int ?? nil
itemObj.Id = item["Id"].string ?? ""
itemObj.ParentIndexNumber = item["ParentIndexNumber"].int ?? nil
itemObj.SeasonId = item["SeasonId"].string ?? nil
itemObj.SeriesId = item["SeriesId"].string ?? nil
itemObj.SeriesName = item["SeriesName"].string ?? nil
}
itemObj.Watched = item["UserData"]["Played"].bool ?? false
_items.wrappedValue.append(itemObj)
}
} catch {
}
break
case .failure(let error):
debugPrint(error)
break
}
isLoading = false;
}
}
@Environment(\.verticalSizeClass) var verticalSizeClass: UserInterfaceSizeClass?
@Environment(\.horizontalSizeClass) var horizontalSizeClass: UserInterfaceSizeClass?
var isPortrait: Bool {
let result = verticalSizeClass == .regular && horizontalSizeClass == .compact
return result
}
var tracks: [GridTrack] {
self.isPortrait ? 3 : 6
}
var body: some View {
VStack() {
NavigationLink(destination: ItemView(item: linkedItem), isActive: $open) {
EmptyView();
};
Spacer().frame(height:6);
TextField("Search", text: $searchQuery, onEditingChanged: { _ in
print("changed")
}, onCommit: {
self.onAppear()
})
.padding(.horizontal, 10)
.foregroundColor(Color.secondary)
.textFieldStyle(RoundedBorderTextFieldStyle())
LoadingView(isShowing: $isLoading) {
GeometryReader { geometry in
Grid(tracks: self.tracks, spacing: GridSpacing(horizontal: 0, vertical: 20)) {
ForEach(items, id: \.Id) { item in
Button() {
_linkedItem.wrappedValue = item;
_close.wrappedValue = false;
_open.wrappedValue = true;
} label: {
VStack(alignment: .leading) {
if(item.Type == "Movie") {
WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?fillWidth=300&fillHeight=450&quality=90&tag=\(item.Image)"))
.resizable()
.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()
.frame(width: 100, height: 150)
.cornerRadius(10)
}
.frame(width:100, height: 150)
.cornerRadius(10)
.shadow(radius: 5)
} else {
WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?fillWidth=300&fillHeight=450&quality=90&tag=\(item.Image)"))
.resizable()
.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()
.frame(width: 100, height: 150)
.cornerRadius(10)
}
.frame(width:100, height: 150)
.cornerRadius(10).overlay(
ZStack {
Text("\(String(item.ItemBadge ?? 0))")
.font(.caption)
.padding(3)
.foregroundColor(.white)
}.background(Color.black)
.opacity(0.8)
.cornerRadius(10.0)
.padding(3), alignment: .topTrailing
)
.shadow(radius: 5)
}
Text(item.Name)
.font(.caption)
.fontWeight(.semibold)
.foregroundColor(.primary)
.lineLimit(1)
Text(String(item.ProductionYear))
.foregroundColor(.secondary)
.font(.caption)
.fontWeight(.medium)
}.frame(width: 100)
}
}
}.gridContentMode(.scroll)
}
}
}.onAppear(perform: onAppear)
.navigationBarTitle("Search", displayMode: .inline)
}
}

View File

@ -26,8 +26,12 @@ struct LibraryView: View {
@State private var viewDidLoad: Bool = false; @State private var viewDidLoad: Bool = false;
@State private var filterString: String = "&SortBy=SortName&SortOrder=Descending"; @State private var filterString: String = "&SortBy=SortName&SortOrder=Descending";
@State private var showFiltersPopover: Bool = false @State private var showFiltersPopover: Bool = false;
@State private var showSearchPopover: Bool = false;
@State private var extraParam: String = "";
@State private var title: String = "";
@State private var url: String = "";
@State private var closeSearch: Bool = false;
var gridItems: [GridItem] = [GridItem(.adaptive(minimum: 150, maximum: 400))] var gridItems: [GridItem] = [GridItem(.adaptive(minimum: 150, maximum: 400))]
@ -35,7 +39,6 @@ struct LibraryView: View {
_prefill_id = State(wrappedValue: prefill ?? "") _prefill_id = State(wrappedValue: prefill ?? "")
_library_names = State(wrappedValue: names) _library_names = State(wrappedValue: names)
_library_ids = State(wrappedValue: libraries) _library_ids = State(wrappedValue: libraries)
//print("prefilling w/ \(prefill ?? "") aka \(names[prefill ?? ""] ?? "nil")")
} }
init(prefill: String?, names: [String: String], libraries: [String], filter: String) { init(prefill: String?, names: [String: String], libraries: [String], filter: String) {
@ -43,7 +46,19 @@ struct LibraryView: View {
_library_names = State(wrappedValue: names) _library_names = State(wrappedValue: names)
_library_ids = State(wrappedValue: libraries) _library_ids = State(wrappedValue: libraries)
_filterString = State(wrappedValue: filter); _filterString = State(wrappedValue: filter);
//print("prefilling w/ \(prefill ?? "") aka \(names[prefill ?? ""] ?? "nil")") }
init(filter: String, extraParams: String, title: String) {
_prefill_id = State(wrappedValue: "erwt");
_filterString = State(wrappedValue: filter);
_extraParam = State(wrappedValue: extraParams);
_title = State(wrappedValue: title)
}
init(extraParams: String, title: String) {
_prefill_id = State(wrappedValue: "erwt");
_extraParam = State(wrappedValue: extraParams);
_title = State(wrappedValue: title)
} }
@State var items: [ResumeItem] = [] @State var items: [ResumeItem] = []
@ -62,9 +77,13 @@ struct LibraryView: View {
func loadItems() { func loadItems() {
_isLoading.wrappedValue = true; _isLoading.wrappedValue = true;
let url = "/Users/\(globalData.user?.user_id ?? "")/Items?Limit=\(endIndex)&StartIndex=\(startIndex)&Recursive=true&Fields=PrimaryImageAspectRatio%2CBasicSyncInfo&ImageTypeLimit=1&EnableImageTypes=Primary%2CBackdrop%2CThumb%2CBanner&IncludeItemTypes=Movie,Series\(selected_library_id == "favorites" ? "&Filters=IsFavorite" : "&ParentId=" + selected_library_id)\(filterString)" if(_extraParam.wrappedValue == "") {
_url.wrappedValue = "/Users/\(globalData.user?.user_id ?? "")/Items?Limit=\(endIndex)&StartIndex=\(startIndex)&Recursive=true&Fields=PrimaryImageAspectRatio%2CBasicSyncInfo&ImageTypeLimit=1&EnableImageTypes=Primary%2CBackdrop%2CThumb%2CBanner&IncludeItemTypes=Movie,Series\(selected_library_id == "favorites" ? "&Filters=IsFavorite" : "&ParentId=" + selected_library_id)\(filterString)"
} else {
_url.wrappedValue = "/Users/\(globalData.user?.user_id ?? "")/Items?Limit=\(endIndex)&StartIndex=\(startIndex)&Recursive=true&Fields=PrimaryImageAspectRatio%2CBasicSyncInfo&ImageTypeLimit=1&EnableImageTypes=Primary%2CBackdrop%2CThumb%2CBanner&IncludeItemTypes=Movie,Series\(filterString)\(extraParam)"
}
let request = RestRequest(method: .get, url: (globalData.server?.baseURI ?? "") + url) let request = RestRequest(method: .get, url: (globalData.server?.baseURI ?? "") + _url.wrappedValue)
request.headerParameters["X-Emby-Authorization"] = globalData.authHeader request.headerParameters["X-Emby-Authorization"] = globalData.authHeader
request.contentType = "application/json" request.contentType = "application/json"
request.acceptType = "application/json" request.acceptType = "application/json"
@ -228,11 +247,10 @@ struct LibraryView: View {
items = []; items = [];
loadItems(); loadItems();
} }
.navigationTitle(library_names[prefill_id] ?? "Library") .navigationTitle(extraParam == "" ? (library_names[prefill_id] ?? "Library") : title)
.toolbar { .toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) { ToolbarItemGroup(placement: .navigationBarTrailing) {
Button { NavigationLink(destination: LibrarySearchView(url: url, close: $closeSearch), isActive: $closeSearch) {
} label: {
Image(systemName: "magnifyingglass") Image(systemName: "magnifyingglass")
} }
Button { Button {
@ -241,7 +259,7 @@ struct LibraryView: View {
Image(systemName: "line.horizontal.3.decrease") Image(systemName: "line.horizontal.3.decrease")
} }
} }
}.popover( isPresented: self.$showFiltersPopover, arrowEdge: .bottom) { LibraryFilterView(library: selected_library_id, output: $filterString, close: $showFiltersPopover) } }.popover( isPresented: self.$showFiltersPopover, arrowEdge: .bottom) { LibraryFilterView(library: selected_library_id, output: $filterString, close: $showFiltersPopover).environmentObject(self.globalData) }
} else { } else {
List(library_ids, id:\.self) { id in List(library_ids, id:\.self) { id in
if(id != "genres") { if(id != "genres") {

View File

@ -35,6 +35,18 @@ class DetailItem: ObservableObject {
@Published var Watched: Bool = false; @Published var Watched: Bool = false;
@Published var Overview: String = ""; @Published var Overview: String = "";
@Published var Tagline: 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] = [];
}
class IVGenre: ObservableObject {
@Published var Id: String = "";
@Published var Name: String = "";
} }
class CastMember: ObservableObject { class CastMember: ObservableObject {
@ -53,8 +65,54 @@ struct MovieItemView: View {
@State private var playing: Bool = false; @State private var playing: Bool = false;
@State private var vc: PreferenceUIHostingController? = nil; @State private var vc: PreferenceUIHostingController? = nil;
@State private var progressString: String = ""; @State private var progressString: String = "";
@State private var watched: Bool = false; @State private var watched: Bool = false {
@State private var favorite: Bool = false; didSet {
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"
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<RestResponse<Data>, RestError>) in
}
} else {
let request = RestRequest(method: .delete, url: (globalData.server?.baseURI ?? "") + "/Users/\(globalData.user?.user_id ?? "")/PlayedItems/\(fullItem.Id)")
request.headerParameters["X-Emby-Authorization"] = globalData.authHeader
request.contentType = "application/json"
request.acceptType = "application/json"
request.responseData() { (result: Result<RestResponse<Data>, 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<RestResponse<Data>, 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<RestResponse<Data>, RestError>) in
}
}
}
};
init(item: ResumeItem) { init(item: ResumeItem) {
self.item = item; self.item = item;
@ -106,6 +164,46 @@ struct MovieItemView: View {
fullItem.Progress = Double(json["UserData"]["PlaybackPositionTicks"].int ?? 0) fullItem.Progress = Double(json["UserData"]["PlaybackPositionTicks"].int ?? 0)
fullItem.OfficialRating = json["OfficialRating"].string ?? "PG-13" fullItem.OfficialRating = json["OfficialRating"].string ?? "PG-13"
fullItem.Watched = json["UserData"]["Played"].bool ?? false; fullItem.Watched = json["UserData"]["Played"].bool ?? false;
fullItem.CommunityRating = String(json["CommunityRating"].float ?? 0.0);
fullItem.CriticRating = String(json["CriticRating"].int ?? 0);
fullItem.ParentId = json["ParentId"].string ?? ""
//People
fullItem.Directors = []
fullItem.Studios = []
fullItem.Writers = []
fullItem.Cast = []
fullItem.Genres = []
for (_,person):(String, JSON) in json["People"] {
if(person["Type"].stringValue == "Director") {
fullItem.Directors.append(person["Name"].string ?? "");
} else if(person["Type"].stringValue == "Writer") {
fullItem.Writers.append(person["Name"].string ?? "");
} else if(person["Type"].stringValue == "Actor") {
let cast = CastMember();
cast.Name = person["Name"].string ?? "";
cast.Id = person["Id"].string ?? "";
let imageTag = person["PrimaryImageTag"].string ?? "";
cast.ImageBlurHash = person["ImageBlurHashes"]["Primary"][imageTag].string ?? "";
cast.Role = person["Role"].string ?? "";
cast.Image = URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(cast.Id)/Images/Primary?fillHeight=744&fillWidth=496&quality=96&tag=\(imageTag)")!
fullItem.Cast.append(cast);
}
}
//Studios
for (_,studio):(String, JSON) in json["Studios"] {
fullItem.Studios.append(studio["Name"].string ?? "");
}
//Genres
for (_,genre):(String, JSON) in json["GenreItems"] {
let tmpGenre = IVGenre()
tmpGenre.Id = genre["Id"].string ?? "";
tmpGenre.Name = genre["Name"].string ?? "";
fullItem.Genres.append(tmpGenre);
}
_watched.wrappedValue = fullItem.Watched _watched.wrappedValue = fullItem.Watched
_favorite.wrappedValue = json["UserData"]["IsFavorite"].bool ?? false; _favorite.wrappedValue = json["UserData"]["IsFavorite"].bool ?? false;
@ -147,49 +245,51 @@ struct MovieItemView: View {
PlayerDemo(item: fullItem, playing: $playing).onAppear(perform: lockOrientations) PlayerDemo(item: fullItem, playing: $playing).onAppear(perform: lockOrientations)
} else { } else {
LoadingView(isShowing: $isLoading) { LoadingView(isShowing: $isLoading) {
ScrollView() { VStack(alignment:.leading) {
VStack(alignment:.leading) { if(!isLoading) {
if(!isLoading) { GeometryReader { geometry in
GeometryReader { geometry in VStack() {
VStack() { WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.Id)/Images/Backdrop?maxWidth=3840&quality=90&tag=\(fullItem.Backdrop)")!)
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
.resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size .placeholder {
.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()
.resizable() .frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing, height: (geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing) * 0.5625)
.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.3) .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) .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) .aspectRatio(contentMode: .fill)
.overlay( .shadow(radius: 5)
HStack() { .overlay(
WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.Id)/Images/Primary?fillWidth=300&fillHeight=450&quality=90&tag=\(fullItem.Poster)")!) HStack() {
.resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.Id)/Images/Primary?fillWidth=300&fillHeight=450&quality=90&tag=\(fullItem.Poster)")!)
.placeholder { .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size
Image(uiImage: UIImage(blurHash: (fullItem.PosterBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : fullItem.PosterBlurHash), size: CGSize(width: 32, height: 32))!) .placeholder {
.resizable() Image(uiImage: UIImage(blurHash: (fullItem.PosterBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : fullItem.PosterBlurHash), size: CGSize(width: 32, height: 32))!)
.frame(width: 120, height: 180) .resizable()
.cornerRadius(10) .frame(width: 120, height: 180)
} .cornerRadius(10)
.frame(width: 120, height: 180) }.aspectRatio(contentMode: .fill)
.cornerRadius(10) .frame(width: 120, height: 180)
VStack(alignment: .leading) { .cornerRadius(10)
Spacer() VStack(alignment: .leading) {
Text(fullItem.Name).font(.headline) Spacer()
.fontWeight(.semibold) Text(fullItem.Name).font(.headline)
.foregroundColor(.primary) .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) .lineLimit(1)
.offset(y: -4) Text(fullItem.Runtime).font(.subheadline)
HStack() { .fontWeight(.medium)
Text(String(fullItem.ProductionYear)).font(.subheadline) .foregroundColor(.secondary)
.fontWeight(.medium) .lineLimit(1)
.foregroundColor(.secondary) if(fullItem.OfficialRating != "") {
.lineLimit(1)
Text(fullItem.Runtime).font(.subheadline)
.fontWeight(.medium)
.foregroundColor(.secondary)
.lineLimit(1)
Text(fullItem.OfficialRating).font(.subheadline) Text(fullItem.OfficialRating).font(.subheadline)
.fontWeight(.semibold) .fontWeight(.semibold)
.foregroundColor(.secondary) .foregroundColor(.secondary)
@ -200,56 +300,138 @@ struct MovieItemView: View {
.stroke(Color.secondary, lineWidth: 1) .stroke(Color.secondary, lineWidth: 1)
) )
} }
if(fullItem.CommunityRating != "") {
}.offset(x: 0, y: -46) HStack() {
}.offset(x: 16, y: 40) Image(systemName: "star").foregroundColor(.secondary)
, alignment: .bottomLeading) Text(fullItem.CommunityRating).font(.subheadline)
VStack(alignment: .leading) { .fontWeight(.semibold)
HStack() { .foregroundColor(.secondary)
//Play button .lineLimit(1)
Button() { .offset(x: -7, y: 0.7)
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)) }.offset(x: 0, y: -46)
.cornerRadius(10) }.offset(x: 16, y: 40)
}.buttonStyle(PlainButtonStyle()) , alignment: .bottomLeading)
.frame(width: 120, height: 25) VStack(alignment: .leading) {
Spacer() HStack() {
//Play button
Button() {
playing = true;
} label: {
HStack() { HStack() {
Button() { Text(fullItem.Progress == 0 ? "Play" : "\(progressString) left").foregroundColor(Color.white).font(.callout).fontWeight(.semibold)
favorite.toggle() Image(systemName: "play.fill").foregroundColor(Color.white).font(.system(size: 20))
} label: { }
if(!favorite) { .frame(width: 120, height: 35)
Image(systemName: "heart").foregroundColor(Color.primary).font(.system(size: 20)) .background(Color(UIColor.systemBlue))
} else { .cornerRadius(10)
Image(systemName: "heart.fill").foregroundColor(Color(UIColor.systemRed)).font(.system(size: 20)) }.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() Button() {
} label: { watched.toggle()
if(watched) { } label: {
Image(systemName: "checkmark.rectangle.fill").foregroundColor(Color.primary).font(.system(size: 20)) if(watched) {
} else { Image(systemName: "checkmark.rectangle.fill").foregroundColor(Color.primary).font(.system(size: 20))
Image(systemName: "xmark.rectangle").foregroundColor(Color.primary).font(.system(size: 20)) } else {
} Image(systemName: "xmark.rectangle").foregroundColor(Color.primary).font(.system(size: 20))
} }
} }
} }
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) ScrollView() {
}.padding(EdgeInsets(top: 24, leading: 16, bottom: 0, trailing: 16)) 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))
} }
} }
} }
}.navigationBarTitleDisplayMode(.inline) }
.navigationTitle("Details") .navigationBarTitleDisplayMode(.inline)
.navigationTitle("Movie Details")
.supportedOrientations(.allButUpsideDown) .supportedOrientations(.allButUpsideDown)
.prefersHomeIndicatorAutoHidden(false) .prefersHomeIndicatorAutoHidden(false)
.withHostingWindow() { window in .withHostingWindow() { window in