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 */; };
|
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;
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -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")))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 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") {
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue