Add movie item image description
This commit is contained in:
parent
e878e4539c
commit
437b71960b
|
@ -12,7 +12,6 @@
|
|||
5338F754263B65E10014BF09 /* SwiftyRequest in Frameworks */ = {isa = PBXBuildFile; productRef = 5338F753263B65E10014BF09 /* SwiftyRequest */; };
|
||||
5338F757263B7E2E0014BF09 /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 5338F756263B7E2E0014BF09 /* KeychainSwift */; };
|
||||
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 */; };
|
||||
535BAEA7264A18AA005FA86D /* PlayerDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535BAEA6264A18AA005FA86D /* PlayerDemo.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 */; };
|
||||
53E4E647263F6CF100F67C6B /* LibraryFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53E4E646263F6CF100F67C6B /* LibraryFilterView.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 */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
|
@ -59,7 +59,6 @@
|
|||
/* Begin PBXFileReference section */
|
||||
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>"; };
|
||||
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>"; };
|
||||
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; };
|
||||
|
@ -82,6 +81,7 @@
|
|||
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>"; };
|
||||
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>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
|
@ -147,9 +147,9 @@
|
|||
53E4E648263F725B00F67C6B /* MultiSelector.swift */,
|
||||
535BAE9E2649E569005FA86D /* ItemView.swift */,
|
||||
53A089CF264DA9DA00D57806 /* MovieItemView.swift */,
|
||||
535BAEA22649E96A005FA86D /* JellyfinHLSResourceLoaderDelegate.swift */,
|
||||
535BAEA4264A151C005FA86D /* VLCPlayer.swift */,
|
||||
535BAEA6264A18AA005FA86D /* PlayerDemo.swift */,
|
||||
53EE24E5265060780068F029 /* LibrarySearchView.swift */,
|
||||
);
|
||||
path = JellyfinPlayer;
|
||||
sourceTree = "<group>";
|
||||
|
@ -278,9 +278,9 @@
|
|||
5377CBF7263B596A003A4E83 /* ContentView.swift in Sources */,
|
||||
5389277C263CC3DB0035E14B /* BlurHashDecode.swift in Sources */,
|
||||
539B2DA5263BA5B8007FF1A4 /* SettingsView.swift in Sources */,
|
||||
535BAEA32649E96A005FA86D /* JellyfinHLSResourceLoaderDelegate.swift in Sources */,
|
||||
5338F74E263B61370014BF09 /* ConnectToServerView.swift in Sources */,
|
||||
5377CBF5263B596A003A4E83 /* JellyfinPlayerApp.swift in Sources */,
|
||||
53EE24E6265060780068F029 /* LibrarySearchView.swift in Sources */,
|
||||
53892772263C8C6F0035E14B /* LoadingView.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
|
|
@ -96,8 +96,8 @@
|
|||
"repositoryURL": "https://github.com/apple/swift-nio.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "21782f3bdc9581148d38d0ccaab6ec952ccda56b",
|
||||
"version": "2.28.0"
|
||||
"revision": "d161bf658780b209c185994528e7e24376cf7283",
|
||||
"version": "2.29.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -114,8 +114,8 @@
|
|||
"repositoryURL": "https://github.com/apple/swift-nio-ssl.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "3d576964a1ace80d2a3f8bab96cab03e5ee074dc",
|
||||
"version": "2.12.0"
|
||||
"revision": "6363cdf6d2fb863e82434f3c4618f4e896e37569",
|
||||
"version": "2.13.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -179,6 +179,7 @@ struct ContentView: View {
|
|||
|
||||
@State private var needsToSelectServer = false;
|
||||
@State private var isSignInErrored = false;
|
||||
@State private var isNetworkErrored = false;
|
||||
@State private var isLoading = false;
|
||||
@State private var tabSelection: String = "Home";
|
||||
@State private var libraries: [String] = [];
|
||||
|
@ -256,9 +257,14 @@ struct ContentView: View {
|
|||
|
||||
}
|
||||
break
|
||||
case .failure( _):
|
||||
_isLoading.wrappedValue = false;
|
||||
_isSignInErrored.wrappedValue = true;
|
||||
case .failure( let error):
|
||||
if(error.response?.status.code == 401) {
|
||||
_isLoading.wrappedValue = false;
|
||||
_isSignInErrored.wrappedValue = true;
|
||||
} else {
|
||||
_isLoading.wrappedValue = false;
|
||||
_isNetworkErrored.wrappedValue = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -323,10 +329,14 @@ struct ContentView: View {
|
|||
Image(systemName: "folder")
|
||||
})
|
||||
.tag("All Media")
|
||||
|
||||
}
|
||||
}.environmentObject(globalData)
|
||||
.onAppear(perform: startup)
|
||||
.navigationViewStyle(StackNavigationViewStyle())
|
||||
.alert(isPresented: $isNetworkErrored) {
|
||||
Alert(title: Text("Network Error"), message: Text("Couldn't connect to Jellyfin"), dismissButton: .default(Text("Ok")))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -26,8 +26,12 @@ struct LibraryView: View {
|
|||
|
||||
@State private var viewDidLoad: Bool = false;
|
||||
@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))]
|
||||
|
||||
|
@ -35,7 +39,6 @@ struct LibraryView: View {
|
|||
_prefill_id = State(wrappedValue: prefill ?? "")
|
||||
_library_names = State(wrappedValue: names)
|
||||
_library_ids = State(wrappedValue: libraries)
|
||||
//print("prefilling w/ \(prefill ?? "") aka \(names[prefill ?? ""] ?? "nil")")
|
||||
}
|
||||
|
||||
init(prefill: String?, names: [String: String], libraries: [String], filter: String) {
|
||||
|
@ -43,7 +46,19 @@ struct LibraryView: View {
|
|||
_library_names = State(wrappedValue: names)
|
||||
_library_ids = State(wrappedValue: libraries)
|
||||
_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] = []
|
||||
|
@ -62,9 +77,13 @@ struct LibraryView: View {
|
|||
|
||||
func loadItems() {
|
||||
_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.contentType = "application/json"
|
||||
request.acceptType = "application/json"
|
||||
|
@ -228,11 +247,10 @@ struct LibraryView: View {
|
|||
items = [];
|
||||
loadItems();
|
||||
}
|
||||
.navigationTitle(library_names[prefill_id] ?? "Library")
|
||||
.navigationTitle(extraParam == "" ? (library_names[prefill_id] ?? "Library") : title)
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
||||
Button {
|
||||
} label: {
|
||||
NavigationLink(destination: LibrarySearchView(url: url, close: $closeSearch), isActive: $closeSearch) {
|
||||
Image(systemName: "magnifyingglass")
|
||||
}
|
||||
Button {
|
||||
|
@ -241,7 +259,7 @@ struct LibraryView: View {
|
|||
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 {
|
||||
List(library_ids, id:\.self) { id in
|
||||
if(id != "genres") {
|
||||
|
|
|
@ -35,6 +35,18 @@ class DetailItem: ObservableObject {
|
|||
@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] = [];
|
||||
}
|
||||
|
||||
class IVGenre: ObservableObject {
|
||||
@Published var Id: String = "";
|
||||
@Published var Name: String = "";
|
||||
}
|
||||
|
||||
class CastMember: ObservableObject {
|
||||
|
@ -53,8 +65,54 @@ struct MovieItemView: View {
|
|||
@State private var playing: Bool = false;
|
||||
@State private var vc: PreferenceUIHostingController? = nil;
|
||||
@State private var progressString: String = "";
|
||||
@State private var watched: Bool = false;
|
||||
@State private var favorite: Bool = false;
|
||||
@State private var watched: 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) {
|
||||
self.item = item;
|
||||
|
@ -106,6 +164,46 @@ struct MovieItemView: View {
|
|||
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.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
|
||||
_favorite.wrappedValue = json["UserData"]["IsFavorite"].bool ?? false;
|
||||
|
||||
|
@ -147,49 +245,51 @@ struct MovieItemView: View {
|
|||
PlayerDemo(item: fullItem, playing: $playing).onAppear(perform: lockOrientations)
|
||||
} else {
|
||||
LoadingView(isShowing: $isLoading) {
|
||||
ScrollView() {
|
||||
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.3)
|
||||
.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)
|
||||
}
|
||||
.frame(width: 120, height: 180)
|
||||
.cornerRadius(10)
|
||||
VStack(alignment: .leading) {
|
||||
Spacer()
|
||||
Text(fullItem.Name).font(.headline)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(.primary)
|
||||
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(
|
||||
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)
|
||||
.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)
|
||||
Text(fullItem.Runtime).font(.subheadline)
|
||||
.fontWeight(.medium)
|
||||
.foregroundColor(.secondary)
|
||||
.lineLimit(1)
|
||||
if(fullItem.OfficialRating != "") {
|
||||
Text(fullItem.OfficialRating).font(.subheadline)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(.secondary)
|
||||
|
@ -200,56 +300,138 @@ struct MovieItemView: View {
|
|||
.stroke(Color.secondary, lineWidth: 1)
|
||||
)
|
||||
}
|
||||
|
||||
}.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))
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(width: 120, height: 35)
|
||||
.background(Color(UIColor.systemBlue))
|
||||
.cornerRadius(10)
|
||||
}.buttonStyle(PlainButtonStyle())
|
||||
.frame(width: 120, height: 25)
|
||||
Spacer()
|
||||
|
||||
}.offset(x: 0, y: -46)
|
||||
}.offset(x: 16, y: 40)
|
||||
, alignment: .bottomLeading)
|
||||
VStack(alignment: .leading) {
|
||||
HStack() {
|
||||
//Play button
|
||||
Button() {
|
||||
playing = true;
|
||||
} label: {
|
||||
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))
|
||||
}
|
||||
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))
|
||||
}
|
||||
}
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
Text(fullItem.Tagline).font(.body).italic().padding(.top, 7).fixedSize(horizontal: false, vertical: true)
|
||||
Text(fullItem.Overview).font(.footnote).padding(.top, 3).fixedSize(horizontal: false, vertical: true)
|
||||
}.padding(EdgeInsets(top: 24, leading: 16, bottom: 0, trailing: 16))
|
||||
}
|
||||
}.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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationTitle("Details")
|
||||
}
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationTitle("Movie Details")
|
||||
.supportedOrientations(.allButUpsideDown)
|
||||
.prefersHomeIndicatorAutoHidden(false)
|
||||
.withHostingWindow() { window in
|
||||
|
|
Loading…
Reference in New Issue