ResumeItem class to struct

LibraryView NavigationLink Recovery
This commit is contained in:
PangMo5 2021-05-27 21:29:30 +09:00
parent 7ada918ea5
commit 3b7778b3cf
18 changed files with 1148 additions and 888 deletions

View File

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1250"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "5377CBF0263B596A003A4E83"
BuildableName = "JellyfinPlayer.app"
BlueprintName = "JellyfinPlayer"
ReferencedContainer = "container:JellyfinPlayer.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "5377CBF0263B596A003A4E83"
BuildableName = "JellyfinPlayer.app"
BlueprintName = "JellyfinPlayer"
ReferencedContainer = "container:JellyfinPlayer.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "5377CBF0263B596A003A4E83"
BuildableName = "JellyfinPlayer.app"
BlueprintName = "JellyfinPlayer"
ReferencedContainer = "container:JellyfinPlayer.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -26,8 +26,11 @@ enum ItemType: String {
} }
enum SortType: String { enum SortType: String {
case name = "Name" case name = "SortName"
case dateCreated = "DateCreated" case dateCreated = "DateCreated"
case datePlayed = "DatePlayed"
case premiereDate = "PremiereDate"
case runtime = "Runtime"
} }
enum ASC: String { enum ASC: String {
@ -37,20 +40,22 @@ enum ASC: String {
enum FilterType: String { enum FilterType: String {
case isFavorite = "IsFavorite" case isFavorite = "IsFavorite"
case isUnplayed = "IsUnplayed"
} }
struct Filter { struct Filter {
var imageTypes = [ImageType]() var imageTypes: [ImageType] = [.primary, .backdrop, .thumb, .banner]
var fields = [Field]() var fields: [Field] = [.primaryImageAspectRatio, .basicSyncInfo]
var itemTypes = [ItemType]() var itemTypes: [ItemType] = [.movie, .series]
var filterTypes = [FilterType]() var filterTypes = [FilterType]()
var sort: SortType? var sort: SortType? = .dateCreated
var asc: ASC? var asc: ASC? = .descending
var parentID: String? var parentID: String?
var imageTypeLimit: Int? var imageTypeLimit: Int? = 1
var recursive = true var recursive = true
var genres = [String]() var genres = [String]()
var personIds = [String]() var personIds = [String]()
var officialRatings = [String]()
} }
extension Filter { extension Filter {
@ -67,6 +72,7 @@ extension Filter {
parameters["SortOrder"] = asc?.rawValue parameters["SortOrder"] = asc?.rawValue
parameters["Genres"] = genres.joined(separator: ",") parameters["Genres"] = genres.joined(separator: ",")
parameters["PersonIds"] = personIds.joined(separator: ",") parameters["PersonIds"] = personIds.joined(separator: ",")
parameters["OfficialRatings"] = officialRatings.joined(separator: ",")
return parameters return parameters
} }
} }
@ -125,7 +131,9 @@ extension JellyfinAPI: TargetType {
case let .items(global, _, _), case let .items(global, _, _),
let .search(global, _, _, _): let .search(global, _, _, _):
return [ return [
"X-Emby-Authorization": global.authHeader "X-Emby-Authorization": global.authHeader,
"Content-Type": "application/json",
"Accept": "application/json"
] ]
} }
} }

View File

@ -197,9 +197,9 @@ struct ContentView: View {
HStack() { HStack() {
Text("Latest \(library_names[library_id] ?? "")").font(.title2).fontWeight(.bold).padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 16)) Text("Latest \(library_names[library_id] ?? "")").font(.title2).fontWeight(.bold).padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 16))
Spacer() Spacer()
// NavigationLink(destination: LibraryView(prefill: library_id, names: [library_id: library_names[library_id] ?? ""], libraries: [library_id], filter: "&SortBy=DateCreated&SortOrder=Descending")) { NavigationLink(destination: LibraryView(viewModel: .init(filter: Filter(parentID: library_id)), title: library_names[library_id] ?? "")) {
Text("See All").font(.subheadline).fontWeight(.bold) Text("See All").font(.subheadline).fontWeight(.bold)
// } }
}.padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16)) }.padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16))
LatestMediaView(library: library_id) LatestMediaView(library: library_id)
}.padding(EdgeInsets(top: 4, leading: 0, bottom: 0, trailing: 0)) }.padding(EdgeInsets(top: 4, leading: 0, bottom: 0, trailing: 0))

View File

@ -64,7 +64,7 @@ struct ContinueWatchingView: View {
let json = try JSON(data: body) let json = try JSON(data: body)
for (_,item):(String, JSON) in json["Items"] { for (_,item):(String, JSON) in json["Items"] {
// Do something you want // Do something you want
let itemObj = ResumeItem() var itemObj = ResumeItem()
if(item["PrimaryImageAspectRatio"].double ?? 0.0 < 1.0) { if(item["PrimaryImageAspectRatio"].double ?? 0.0 < 1.0) {
//portrait; use backdrop instead //portrait; use backdrop instead
itemObj.Image = item["BackdropImageTags"][0].string ?? "" itemObj.Image = item["BackdropImageTags"][0].string ?? ""

View File

@ -12,9 +12,9 @@ import Moya
import SwiftyJSON import SwiftyJSON
final class LibraryViewModel: ObservableObject { final class LibraryViewModel: ObservableObject {
fileprivate var provider = MoyaProvider<JellyfinAPI>(plugins: [NetworkLoggerPlugin(configuration: NetworkLoggerPlugin.Configuration(logOptions: .verbose))]) fileprivate var provider =
MoyaProvider<JellyfinAPI>(plugins: [NetworkLoggerPlugin()])
var prefillID: String
@Published @Published
var filter: Filter var filter: Filter
@ -31,64 +31,59 @@ final class LibraryViewModel: ObservableObject {
var page = 1 var page = 1
var globalData = GlobalData() var globalData = GlobalData() {
didSet {
injectEnvironmentData()
}
}
fileprivate var cancellables = Set<AnyCancellable>() fileprivate var cancellables = Set<AnyCancellable>()
init(prefillID: String, init(filter: Filter = Filter()) {
filter: Filter? = nil) self.filter = filter
{ }
self.prefillID = prefillID
if let unwrappedFilter = filter { fileprivate func injectEnvironmentData() {
self.filter = unwrappedFilter cancellables.removeAll()
} else {
self.filter = Filter(imageTypes: [.primary, .backdrop, .thumb, .banner], $filter
fields: [.primaryImageAspectRatio, .basicSyncInfo], .sink(receiveValue: requestInitItems(_:))
itemTypes: [.movie, .series], .store(in: &cancellables)
sort: .dateCreated,
asc: .descending,
parentID: prefillID,
imageTypeLimit: 1,
recursive: true)
}
} }
func requestNextPage() { func requestNextPage() {
page += 1 page += 1
requestItems() requestItems(filter)
} }
func requestPreviousPage() { func requestPreviousPage() {
page -= 1 page -= 1
requestItems() requestItems(filter)
}
func requestInitItems() {
page = 1
requestItems()
} }
fileprivate func requestItems() { func requestInitItems(_ filter: Filter) {
print(globalData.server?.baseURI) page = 1
requestItems(filter)
}
fileprivate func requestItems(_ filter: Filter) {
print("ASDASDA")
print(globalData.authHeader) print(globalData.authHeader)
print(filter)
isLoading = true isLoading = true
provider.requestPublisher(.items(globalData: globalData, filter: filter, page: page)) provider.requestPublisher(.items(globalData: globalData, filter: filter, page: page))
// .map(ResumeItem.self) TO DO // .map(ResumeItem.self) TO DO
.print() .print()
.sink(receiveCompletion: { _ in .receive(on: DispatchQueue.main)
self.isLoading = false .map { response -> ([ResumeItem], Int) in
}, receiveValue: { response in
self.items.removeAll()
let body = response.data let body = response.data
var totalCount = 0 var totalCount = 0
var innerItems = [ResumeItem]()
do { do {
let json = try JSON(data: body) let json = try JSON(data: body)
totalCount = json["TotalRecordCount"].int ?? 0 totalCount = json["TotalRecordCount"].int ?? 0
for (_, item): (String, JSON) in json["Items"] { for (_, item): (String, JSON) in json["Items"] {
// Do something you want // Do something you want
let itemObj = ResumeItem() var itemObj = ResumeItem()
itemObj.Type = item["Type"].string ?? "" itemObj.Type = item["Type"].string ?? ""
if itemObj.Type == "Series" { if itemObj.Type == "Series" {
itemObj.ItemBadge = item["UserData"]["UnplayedItemCount"].int ?? 0 itemObj.ItemBadge = item["UserData"]["UnplayedItemCount"].int ?? 0
@ -120,21 +115,29 @@ final class LibraryViewModel: ObservableObject {
} }
itemObj.Watched = item["UserData"]["Played"].bool ?? false itemObj.Watched = item["UserData"]["Played"].bool ?? false
self.items.append(itemObj) innerItems.append(itemObj)
} }
} catch {} } catch {}
return (innerItems, totalCount)
if totalCount > 100 { }
.sink(receiveCompletion: { [weak self] _ in
guard let self = self else { return }
self.isLoading = false
}, receiveValue: { [weak self] items, count in
guard let self = self else { return }
if count > 100 {
if self.page > 1 { if self.page > 1 {
self.isHiddenPreviousButton = false self.isHiddenPreviousButton = false
} }
if totalCount > (self.page * 100) { if count > (self.page * 100) {
self.isHiddenNextButton = false self.isHiddenNextButton = false
} }
} else { } else {
self.isHiddenNextButton = true self.isHiddenNextButton = true
self.isHiddenPreviousButton = true self.isHiddenPreviousButton = true
} }
self.items = items
}) })
.store(in: &cancellables) .store(in: &cancellables)
} }

View File

@ -52,16 +52,18 @@ final class LibrarySearchViewModel: ObservableObject {
provider.requestPublisher(.search(globalData: globalData, filter: filter, searchQuery: query, page: page)) provider.requestPublisher(.search(globalData: globalData, filter: filter, searchQuery: query, page: page))
// .map(ResumeItem.self) TO DO // .map(ResumeItem.self) TO DO
.print() .print()
.sink(receiveCompletion: { _ in .sink(receiveCompletion: { [weak self] _ in
guard let self = self else { return }
self.isLoading = false self.isLoading = false
}, receiveValue: { response in }, receiveValue: { [weak self] response in
guard let self = self else { return }
let body = response.data let body = response.data
self.items.removeAll() var innerItems = [ResumeItem]()
do { do {
let json = try JSON(data: body) let json = try JSON(data: body)
for (_, item): (String, JSON) in json["Items"] { for (_, item): (String, JSON) in json["Items"] {
// Do something you want // Do something you want
let itemObj = ResumeItem() var itemObj = ResumeItem()
itemObj.Type = item["Type"].string ?? "" itemObj.Type = item["Type"].string ?? ""
if itemObj.Type == "Series" { if itemObj.Type == "Series" {
itemObj.ItemBadge = item["UserData"]["UnplayedItemCount"].int ?? 0 itemObj.ItemBadge = item["UserData"]["UnplayedItemCount"].int ?? 0
@ -93,9 +95,10 @@ final class LibrarySearchViewModel: ObservableObject {
} }
itemObj.Watched = item["UserData"]["Played"].bool ?? false itemObj.Watched = item["UserData"]["Played"].bool ?? false
self.items.append(itemObj) innerItems.append(itemObj)
} }
} catch {} } catch {}
self.items = innerItems
}) })
.store(in: &cancellables) .store(in: &cancellables)
} }

View File

@ -5,91 +5,107 @@
// Created by Aiden Vigue on 5/13/21. // Created by Aiden Vigue on 5/13/21.
// //
import SwiftUI
import SwiftyRequest
import SwiftyJSON
import SDWebImageSwiftUI import SDWebImageSwiftUI
import SwiftUI
import SwiftyJSON
import SwiftyRequest
struct EpisodeItemView: View { struct EpisodeItemView: View {
@EnvironmentObject private var globalData: GlobalData @EnvironmentObject
@EnvironmentObject private var orientationInfo: OrientationInfo private var globalData: GlobalData
@EnvironmentObject private var playbackInfo: ItemPlayback @EnvironmentObject
var item: ResumeItem; private var orientationInfo: OrientationInfo
var fullItem: DetailItem; @EnvironmentObject
private var playbackInfo: ItemPlayback
var item: ResumeItem
var fullItem: DetailItem
@State private var isLoading: Bool = true; @State
@State private var progressString: String = ""; private var isLoading: Bool = true
@State private var viewDidLoad: Bool = false; @State
private var progressString: String = ""
@State private var watched: Bool = false { @State
private var viewDidLoad: Bool = false
@State
private var watched: Bool = false {
didSet { didSet {
if(watched == true) { if watched == true {
let date = Date() let date = Date()
let formatter = DateFormatter() let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX") formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(secondsFromGMT: 0) formatter.timeZone = TimeZone(secondsFromGMT: 0)
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ" formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ"
let request = RestRequest(method: .post, url: (globalData.server?.baseURI ?? "") + "/Users/\(globalData.user?.user_id ?? "")/PlayedItems/\(fullItem.Id)?DatePlayed=\(formatter.string(from: date).replacingOccurrences(of: ":", with: "%3A"))") let request = RestRequest(method: .post,
url: (globalData.server?.baseURI ?? "") +
"/Users/\(globalData.user?.user_id ?? "")/PlayedItems/\(fullItem.Id)?DatePlayed=\(formatter.string(from: date).replacingOccurrences(of: ":", with: "%3A"))")
request.headerParameters["X-Emby-Authorization"] = globalData.authHeader request.headerParameters["X-Emby-Authorization"] = globalData.authHeader
request.contentType = "application/json" request.contentType = "application/json"
request.acceptType = "application/json" request.acceptType = "application/json"
request.responseData() { (result: Result<RestResponse<Data>, RestError>) in request.responseData { (_: Result<RestResponse<Data>, RestError>) in
} }
} else { } else {
let request = RestRequest(method: .delete, url: (globalData.server?.baseURI ?? "") + "/Users/\(globalData.user?.user_id ?? "")/PlayedItems/\(fullItem.Id)") let request = RestRequest(method: .delete,
url: (globalData.server?.baseURI ?? "") +
"/Users/\(globalData.user?.user_id ?? "")/PlayedItems/\(fullItem.Id)")
request.headerParameters["X-Emby-Authorization"] = globalData.authHeader request.headerParameters["X-Emby-Authorization"] = globalData.authHeader
request.contentType = "application/json" request.contentType = "application/json"
request.acceptType = "application/json" request.acceptType = "application/json"
request.responseData() { (result: Result<RestResponse<Data>, RestError>) in request.responseData { (_: 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;
fullItem = DetailItem();
} }
@State
private var favorite: Bool = false {
didSet {
if favorite == true {
let request = RestRequest(method: .post,
url: (globalData.server?.baseURI ?? "") +
"/Users/\(globalData.user?.user_id ?? "")/FavoriteItems/\(fullItem.Id)")
request.headerParameters["X-Emby-Authorization"] = globalData.authHeader
request.contentType = "application/json"
request.acceptType = "application/json"
request.responseData { (_: Result<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<RestResponse<Data>, RestError>) in
}
}
}
}
init(item: ResumeItem) {
self.item = item
self.fullItem = DetailItem()
}
func loadData() { func loadData() {
if(_viewDidLoad.wrappedValue == true) { if _viewDidLoad.wrappedValue == true {
return return
} }
_viewDidLoad.wrappedValue = true; _viewDidLoad.wrappedValue = true
let url = "/Users/\(globalData.user?.user_id ?? "")/Items/\(item.Id)" let url = "/Users/\(globalData.user?.user_id ?? "")/Items/\(item.Id)"
let request = RestRequest(method: .get, url: (globalData.server?.baseURI ?? "") + url) let request = RestRequest(method: .get, url: (globalData.server?.baseURI ?? "") + url)
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"
request.responseData() { (result: Result<RestResponse<Data>, RestError>) in request.responseData { (result: Result<RestResponse<Data>, RestError>) in
switch result { switch result {
case .success(let response): case let .success(response):
let body = response.body let body = response.body
do { do {
let json = try JSON(data: body) let json = try JSON(data: body)
@ -110,225 +126,256 @@ struct EpisodeItemView: View {
fullItem.SeriesName = json["SeriesName"].string ?? nil fullItem.SeriesName = json["SeriesName"].string ?? nil
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.CommunityRating = String(json["CommunityRating"].float ?? 0.0)
fullItem.CriticRating = String(json["CriticRating"].int ?? 0); fullItem.CriticRating = String(json["CriticRating"].int ?? 0)
fullItem.ParentId = json["ParentId"].string ?? "" fullItem.ParentId = json["ParentId"].string ?? ""
fullItem.ParentBackdropItemId = json["ParentBackdropItemId"].string ?? "" fullItem.ParentBackdropItemId = json["ParentBackdropItemId"].string ?? ""
//People // People
fullItem.Directors = [] fullItem.Directors = []
fullItem.Studios = [] fullItem.Studios = []
fullItem.Writers = [] fullItem.Writers = []
fullItem.Cast = [] fullItem.Cast = []
fullItem.Genres = [] fullItem.Genres = []
for (_,person):(String, JSON) in json["People"] { for (_, person): (String, JSON) in json["People"] {
if(person["Type"].stringValue == "Director") { if person["Type"].stringValue == "Director" {
fullItem.Directors.append(person["Name"].string ?? ""); fullItem.Directors.append(person["Name"].string ?? "")
} else if(person["Type"].stringValue == "Writer") { } else if person["Type"].stringValue == "Writer" {
fullItem.Writers.append(person["Name"].string ?? ""); fullItem.Writers.append(person["Name"].string ?? "")
} else if(person["Type"].stringValue == "Actor") { } else if person["Type"].stringValue == "Actor" {
let cast = CastMember(); let cast = CastMember()
cast.Name = person["Name"].string ?? ""; cast.Name = person["Name"].string ?? ""
cast.Id = person["Id"].string ?? ""; cast.Id = person["Id"].string ?? ""
let imageTag = person["PrimaryImageTag"].string ?? ""; let imageTag = person["PrimaryImageTag"].string ?? ""
cast.ImageBlurHash = person["ImageBlurHashes"]["Primary"][imageTag].string ?? ""; cast.ImageBlurHash = person["ImageBlurHashes"]["Primary"][imageTag].string ?? ""
cast.Role = person["Role"].string ?? ""; cast.Role = person["Role"].string ?? ""
cast.Image = URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(cast.Id)/Images/Primary?maxHeight=250&quality=85&tag=\(imageTag)")! cast
fullItem.Cast.append(cast); .Image =
URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(cast.Id)/Images/Primary?maxHeight=250&quality=85&tag=\(imageTag)")!
fullItem.Cast.append(cast)
} }
} }
//Studios // Studios
for (_,studio):(String, JSON) in json["Studios"] { for (_, studio): (String, JSON) in json["Studios"] {
fullItem.Studios.append(studio["Name"].string ?? ""); fullItem.Studios.append(studio["Name"].string ?? "")
} }
//Genres // Genres
for (_,genre):(String, JSON) in json["GenreItems"] { for (_, genre): (String, JSON) in json["GenreItems"] {
let tmpGenre = IVGenre() let tmpGenre = IVGenre()
tmpGenre.Id = genre["Id"].string ?? ""; tmpGenre.Id = genre["Id"].string ?? ""
tmpGenre.Name = genre["Name"].string ?? ""; tmpGenre.Name = genre["Name"].string ?? ""
fullItem.Genres.append(tmpGenre); 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
//Process runtime // Process runtime
let seconds: Int = ((json["RunTimeTicks"].int ?? 0)/10000000) let seconds: Int = ((json["RunTimeTicks"].int ?? 0) / 10_000_000)
fullItem.RuntimeTicks = json["RunTimeTicks"].int ?? 0; fullItem.RuntimeTicks = json["RunTimeTicks"].int ?? 0
let hours = (seconds/3600) let hours = (seconds / 3600)
let minutes = ((seconds - (hours * 3600))/60) let minutes = ((seconds - (hours * 3600)) / 60)
if(hours != 0) { if hours != 0 {
fullItem.Runtime = "\(hours):\(String(minutes).leftPad(toWidth: 2, withString: "0"))" fullItem.Runtime = "\(hours):\(String(minutes).leftPad(toWidth: 2, withString: "0"))"
} else { } else {
fullItem.Runtime = "\(String(minutes).leftPad(toWidth: 2, withString: "0"))m" fullItem.Runtime = "\(String(minutes).leftPad(toWidth: 2, withString: "0"))m"
} }
if(fullItem.Progress != 0) { if fullItem.Progress != 0 {
let remainingSecs = (Double(json["RunTimeTicks"].int ?? 0) - fullItem.Progress)/10000000 let remainingSecs = (Double(json["RunTimeTicks"].int ?? 0) - fullItem.Progress) / 10_000_000
let proghours = Int(remainingSecs/3600) let proghours = Int(remainingSecs / 3600)
let progminutes = Int((Int(remainingSecs) - (proghours * 3600))/60) let progminutes = Int((Int(remainingSecs) - (proghours * 3600)) / 60)
if(proghours != 0) { if proghours != 0 {
_progressString.wrappedValue = "\(proghours):\(String(progminutes).leftPad(toWidth: 2, withString: "0"))" _progressString.wrappedValue = "\(proghours):\(String(progminutes).leftPad(toWidth: 2, withString: "0"))"
} else { } else {
_progressString.wrappedValue = "\(String(progminutes).leftPad(toWidth: 2, withString: "0"))m" _progressString.wrappedValue = "\(String(progminutes).leftPad(toWidth: 2, withString: "0"))m"
} }
} }
} catch { } catch {}
case let .failure(error):
}
break
case .failure(let error):
debugPrint(error) debugPrint(error)
break
} }
_isLoading.wrappedValue = false; _isLoading.wrappedValue = false
} }
} }
var body: some View { var body: some View {
LoadingView(isShowing: $isLoading) { LoadingView(isShowing: $isLoading) {
VStack(alignment:.leading) { VStack(alignment: .leading) {
if(!isLoading) { if !isLoading {
if(orientationInfo.orientation == .portrait) { if orientationInfo.orientation == .portrait {
GeometryReader { geometry in GeometryReader { geometry in
VStack() { VStack {
WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.ParentBackdropItemId)/Images/Backdrop?maxWidth=550&quality=90&tag=\(fullItem.Backdrop)")!) WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.ParentBackdropItemId)/Images/Backdrop?maxWidth=550&quality=90&tag=\(fullItem.Backdrop)")!)
.resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size .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: UIDevice.current.userInterfaceIdiom == .pad ? 350 : (geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing) * 0.5625) .frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets
.trailing,
height: UIDevice.current
.userInterfaceIdiom == .pad ? 350 :
(geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets
.trailing) * 0.5625)
} }
.opacity(0.3) .opacity(0.3)
.aspectRatio(contentMode: .fill) .aspectRatio(contentMode: .fill)
.frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing, height: UIDevice.current.userInterfaceIdiom == .pad ? 350 : (geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing) * 0.5625) .frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing,
height: UIDevice.current
.userInterfaceIdiom == .pad ? 350 :
(geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing) *
0.5625)
.shadow(radius: 5) .shadow(radius: 5)
.overlay( .overlay(HStack {
HStack() { WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.SeriesId ?? "")/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)")!)
WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.SeriesId ?? "")/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)")!) .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size
.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
Image(uiImage: UIImage(blurHash: (fullItem.PosterBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : fullItem.PosterBlurHash), size: CGSize(width: 32, height: 32))!) .PosterBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" :
.resizable() fullItem.PosterBlurHash,
.frame(width: 120, height: 180) size: CGSize(width: 32, height: 32))!)
.cornerRadius(10) .resizable()
}.aspectRatio(contentMode: .fill) .frame(width: 120, height: 180)
.frame(width: 120, height: 180) .cornerRadius(10)
.cornerRadius(10) }.aspectRatio(contentMode: .fill)
VStack(alignment: .leading) { .frame(width: 120, height: 180)
Spacer() .cornerRadius(10)
Text(fullItem.Name).font(.headline) VStack(alignment: .leading) {
.fontWeight(.semibold) Spacer()
.foregroundColor(.primary) Text(fullItem.Name).font(.headline)
.fixedSize(horizontal: false, vertical: true) .fontWeight(.semibold)
.offset(y: -4) .foregroundColor(.primary)
HStack() { .fixedSize(horizontal: false, vertical: true)
Text(String(fullItem.ProductionYear)).font(.subheadline) .offset(y: -4)
.fontWeight(.medium) HStack {
Text(String(fullItem.ProductionYear)).font(.subheadline)
.fontWeight(.medium)
.foregroundColor(.secondary)
.lineLimit(1)
Text(fullItem.Runtime).font(.subheadline)
.fontWeight(.medium)
.foregroundColor(.secondary)
.lineLimit(1)
if fullItem.OfficialRating != "" {
Text(fullItem.OfficialRating).font(.subheadline)
.fontWeight(.semibold)
.foregroundColor(.secondary) .foregroundColor(.secondary)
.lineLimit(1) .lineLimit(1)
Text(fullItem.Runtime).font(.subheadline) .padding(EdgeInsets(top: 1, leading: 4, bottom: 1, trailing: 4))
.fontWeight(.medium) .overlay(RoundedRectangle(cornerRadius: 2)
.foregroundColor(.secondary) .stroke(Color.secondary, lineWidth: 1))
.lineLimit(1) }
if(fullItem.OfficialRating != "") { if fullItem.CommunityRating != "0" {
Text(fullItem.OfficialRating).font(.subheadline) HStack {
Image(systemName: "star").foregroundColor(.secondary)
Text(fullItem.CommunityRating).font(.subheadline)
.fontWeight(.semibold) .fontWeight(.semibold)
.foregroundColor(.secondary) .foregroundColor(.secondary)
.lineLimit(1) .lineLimit(1)
.padding(EdgeInsets(top: 1, leading: 4, bottom: 1, trailing: 4)) .offset(x: -7, y: 0.7)
.overlay(
RoundedRectangle(cornerRadius: 2)
.stroke(Color.secondary, lineWidth: 1)
)
} }
if(fullItem.CommunityRating != "0") { }
HStack() { }.frame(maxWidth: .infinity, alignment: .leading)
Image(systemName: "star").foregroundColor(.secondary) }.frame(maxWidth: .infinity, alignment: .leading)
Text(fullItem.CommunityRating).font(.subheadline) .offset(x: 0, y: UIDevice.current.userInterfaceIdiom == .pad ? -98 : -46)
.fontWeight(.semibold) .padding(.trailing, 16)
.foregroundColor(.secondary) }.offset(x: 16, y: UIDevice.current.userInterfaceIdiom == .pad ? 135 : 40),
.lineLimit(1) alignment: .bottomLeading)
.offset(x: -7, y: 0.7)
}
}
}.frame(maxWidth: .infinity, alignment: .leading)
}.frame(maxWidth: .infinity, alignment: .leading).offset(x: 0, y: UIDevice.current.userInterfaceIdiom == .pad ? -98 : -46).padding(.trailing, 16)
}.offset(x: 16, y: UIDevice.current.userInterfaceIdiom == .pad ? 135 : 40)
, alignment: .bottomLeading)
VStack(alignment: .leading) { VStack(alignment: .leading) {
HStack() { HStack {
//Play button // Play button
Button { Button {
self.playbackInfo.itemToPlay = fullItem; self.playbackInfo.itemToPlay = fullItem
self.playbackInfo.shouldPlay = true; self.playbackInfo.shouldPlay = true
} label: { } label: {
HStack() { HStack {
Text(fullItem.Progress == 0 ? "Play" : "\(progressString) left").foregroundColor(Color.white).font(.callout).fontWeight(.semibold) 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)) Image(systemName: "play.fill").foregroundColor(Color.white).font(.system(size: 20))
} }
.frame(width: 120, height: 35) .frame(width: 120, height: 35)
.background(Color(red: 172/255, green: 92/255, blue: 195/255)) .background(Color(red: 172 / 255, green: 92 / 255, blue: 195 / 255))
.cornerRadius(10) .cornerRadius(10)
}.buttonStyle(PlainButtonStyle()) }.buttonStyle(PlainButtonStyle())
.frame(width: 120, height: 35) .frame(width: 120, height: 35)
Spacer() Spacer()
HStack() { HStack {
Button() { Button {
favorite.toggle() favorite.toggle()
} label: { } label: {
if(!favorite) { if !favorite {
Image(systemName: "heart").foregroundColor(Color.primary).font(.system(size: 20)) Image(systemName: "heart").foregroundColor(Color.primary).font(.system(size: 20))
} else { } else {
Image(systemName: "heart.fill").foregroundColor(Color(UIColor.systemRed)).font(.system(size: 20)) Image(systemName: "heart.fill").foregroundColor(Color(UIColor.systemRed))
.font(.system(size: 20))
} }
} }
Button() { Button {
watched.toggle() watched.toggle()
} label: { } label: {
if(watched) { if watched {
Image(systemName: "checkmark.rectangle.fill").foregroundColor(Color.primary).font(.system(size: 20)) Image(systemName: "checkmark.rectangle.fill").foregroundColor(Color.primary)
.font(.system(size: 20))
} else { } else {
Image(systemName: "xmark.rectangle").foregroundColor(Color.primary).font(.system(size: 20)) Image(systemName: "xmark.rectangle").foregroundColor(Color.primary)
.font(.system(size: 20))
} }
} }
} }
}.padding(.leading, 16).padding(.trailing,16) }.padding(.leading, 16).padding(.trailing, 16)
ScrollView() { ScrollView {
VStack(alignment: .leading) { VStack(alignment: .leading) {
if(fullItem.Tagline != "") { 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.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) Text(fullItem.Overview).font(.footnote).padding(.top, 3)
if(fullItem.Genres.count != 0) { .fixedSize(horizontal: false, vertical: true).padding(.bottom, 3).padding(.leading, 16)
.padding(.trailing, 16)
if !fullItem.Genres.isEmpty {
ScrollView(.horizontal, showsIndicators: false) { ScrollView(.horizontal, showsIndicators: false) {
HStack() { HStack {
Text("Genres:").font(.callout).fontWeight(.semibold) Text("Genres:").font(.callout).fontWeight(.semibold)
ForEach(fullItem.Genres, id: \.Id) {genre in ForEach(fullItem.Genres, id: \.Id) { genre in
// NavigationLink(destination: LibraryView(extraParams: "&Genres=\(genre.Name.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "")", title: genre.Name)) { NavigationLink(destination: LibraryView(viewModel: .init(filter: Filter(genres: [
genre
.Name,
])), title: genre.Name)) {
Text(genre.Name).font(.footnote) Text(genre.Name).font(.footnote)
// } }
} }
}.padding(.leading, 16).padding(.trailing,16) }.padding(.leading, 16).padding(.trailing, 16)
} }
} }
if(fullItem.Cast.count != 0) { if !fullItem.Cast.isEmpty {
ScrollView(.horizontal, showsIndicators: false) { ScrollView(.horizontal, showsIndicators: false) {
VStack() { VStack {
Spacer().frame(height: 8); Spacer().frame(height: 8)
HStack() { HStack {
Spacer().frame(width: 16) Spacer().frame(width: 16)
ForEach(fullItem.Cast, id: \.Id) { cast in ForEach(fullItem.Cast, id: \.Id) { cast in
// NavigationLink(destination: LibraryView(extraParams: "&PersonIds=\(cast.Id)", title: cast.Name)) { NavigationLink(destination: LibraryView(viewModel: .init(filter: Filter(personIds: [
VStack() { cast
.Id,
])), title: cast.Name)) {
VStack {
WebImage(url: cast.Image) WebImage(url: cast.Image)
.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: (cast.ImageBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : cast.ImageBlurHash), size: CGSize(width: 16, height: 16))!) Image(uiImage: UIImage(blurHash: cast
.ImageBlurHash == "" ?
"W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" :
cast.ImageBlurHash,
size: CGSize(width: 16,
height: 16))!)
.resizable() .resizable()
.aspectRatio(contentMode: .fill) .aspectRatio(contentMode: .fill)
.frame(width: 100, height: 100) .frame(width: 100, height: 100)
@ -337,12 +384,14 @@ struct EpisodeItemView: View {
.aspectRatio(contentMode: .fill) .aspectRatio(contentMode: .fill)
.frame(width: 100, height: 100) .frame(width: 100, height: 100)
.cornerRadius(10) .cornerRadius(10)
Text(cast.Name).font(.footnote).fontWeight(.regular).lineLimit(1).frame(width: 100).foregroundColor(Color.primary) Text(cast.Name).font(.footnote).fontWeight(.regular).lineLimit(1)
if(cast.Role != "") { .frame(width: 100).foregroundColor(Color.primary)
Text(cast.Role).font(.caption).fontWeight(.medium).lineLimit(1).foregroundColor(Color.secondary).frame(width: 100) if cast.Role != "" {
Text(cast.Role).font(.caption).fontWeight(.medium).lineLimit(1)
.foregroundColor(Color.secondary).frame(width: 100)
} }
} }
// } }
Spacer().frame(width: 10) Spacer().frame(width: 10)
} }
Spacer().frame(width: 16) Spacer().frame(width: 16)
@ -350,51 +399,66 @@ struct EpisodeItemView: View {
} }
}.padding(.top, -3) }.padding(.top, -3)
} }
if(fullItem.Directors.count != 0) { if !fullItem.Directors.isEmpty {
HStack() { HStack {
Text("Directors:").font(.callout).fontWeight(.semibold) Text("Directors:").font(.callout).fontWeight(.semibold)
Text(fullItem.Directors.joined(separator: ", ")).font(.footnote).lineLimit(1).foregroundColor(Color.secondary) Text(fullItem.Directors.joined(separator: ", ")).font(.footnote).lineLimit(1)
}.padding(.leading, 16).padding(.trailing,16) .foregroundColor(Color.secondary)
}.padding(.leading, 16).padding(.trailing, 16)
} }
if(fullItem.Writers.count != 0) { if !fullItem.Writers.isEmpty {
HStack() { HStack {
Text("Writers:").font(.callout).fontWeight(.semibold) Text("Writers:").font(.callout).fontWeight(.semibold)
Text(fullItem.Writers.joined(separator: ", ")).font(.footnote).lineLimit(1).foregroundColor(Color.secondary) Text(fullItem.Writers.joined(separator: ", ")).font(.footnote).lineLimit(1)
}.padding(.leading, 16).padding(.trailing,16) .foregroundColor(Color.secondary)
}.padding(.leading, 16).padding(.trailing, 16)
} }
if(fullItem.Studios.count != 0) { if !fullItem.Studios.isEmpty {
HStack() { HStack {
Text("Studios:").font(.callout).fontWeight(.semibold) Text("Studios:").font(.callout).fontWeight(.semibold)
Text(fullItem.Studios.joined(separator: ", ")).font(.footnote).lineLimit(1).foregroundColor(Color.secondary) Text(fullItem.Studios.joined(separator: ", ")).font(.footnote).lineLimit(1)
}.padding(.leading, 16).padding(.trailing,16) .foregroundColor(Color.secondary)
}.padding(.leading, 16).padding(.trailing, 16)
} }
Spacer().frame(height: 3) Spacer().frame(height: 3)
} }
} }
}.padding(EdgeInsets(top: UIDevice.current.userInterfaceIdiom == .pad ? 54 : 24, leading: 0, bottom: 0, trailing: 0)) }
.padding(EdgeInsets(top: UIDevice.current.userInterfaceIdiom == .pad ? 54 : 24, leading: 0, bottom: 0,
trailing: 0))
} }
} }
} else { } else {
GeometryReader { geometry in GeometryReader { geometry in
ZStack() { ZStack {
WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.ParentBackdropItemId)/Images/Backdrop?maxWidth=750&quality=90&tag=\(fullItem.Backdrop)")!) WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.ParentBackdropItemId)/Images/Backdrop?maxWidth=750&quality=90&tag=\(fullItem.Backdrop)")!)
.resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size .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.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom) .frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets
.trailing,
height: geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets
.bottom)
} }
.opacity(0.3) .opacity(0.3)
.aspectRatio(contentMode: .fill) .aspectRatio(contentMode: .fill)
.frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing, height: geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom) .frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing,
height: geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom)
.edgesIgnoringSafeArea(.all) .edgesIgnoringSafeArea(.all)
HStack() { HStack {
VStack() { VStack {
WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.SeriesId ?? "")/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)")!) WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.SeriesId ?? "")/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)")!)
.resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size .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.PosterBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : fullItem.PosterBlurHash), size: CGSize(width: 32, height: 32))!) Image(uiImage: UIImage(blurHash: fullItem
.PosterBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" :
fullItem.PosterBlurHash,
size: CGSize(width: 32, height: 32))!)
.resizable() .resizable()
.frame(width: 120, height: 180) .frame(width: 120, height: 180)
} }
@ -404,23 +468,24 @@ struct EpisodeItemView: View {
.shadow(radius: 5) .shadow(radius: 5)
Spacer().frame(height: 15) Spacer().frame(height: 15)
Button { Button {
self.playbackInfo.itemToPlay = fullItem; self.playbackInfo.itemToPlay = fullItem
self.playbackInfo.shouldPlay = true; self.playbackInfo.shouldPlay = true
} label: { } label: {
HStack() { HStack {
Text(fullItem.Progress == 0 ? "Play" : "\(progressString) left").foregroundColor(Color.white).font(.callout).fontWeight(.semibold) 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)) Image(systemName: "play.fill").foregroundColor(Color.white).font(.system(size: 20))
} }
.frame(width: 120, height: 35) .frame(width: 120, height: 35)
.background(Color(red: 172/255, green: 92/255, blue: 195/255)) .background(Color(red: 172 / 255, green: 92 / 255, blue: 195 / 255))
.cornerRadius(10) .cornerRadius(10)
}.buttonStyle(PlainButtonStyle()) }.buttonStyle(PlainButtonStyle())
.frame(width: 120, height: 35) .frame(width: 120, height: 35)
Spacer() Spacer()
} }
ScrollView() { ScrollView {
VStack(alignment: .leading) { VStack(alignment: .leading) {
HStack() { HStack {
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text(fullItem.Name).font(.headline) Text(fullItem.Name).font(.headline)
.fontWeight(.semibold) .fontWeight(.semibold)
@ -428,7 +493,7 @@ struct EpisodeItemView: View {
.fixedSize(horizontal: false, vertical: true) .fixedSize(horizontal: false, vertical: true)
.offset(x: 14, y: 0) .offset(x: 14, y: 0)
Spacer().frame(height: 1) Spacer().frame(height: 1)
HStack() { HStack {
Text(String(fullItem.ProductionYear)).font(.subheadline) Text(String(fullItem.ProductionYear)).font(.subheadline)
.fontWeight(.medium) .fontWeight(.medium)
.foregroundColor(.secondary) .foregroundColor(.secondary)
@ -437,19 +502,17 @@ struct EpisodeItemView: View {
.fontWeight(.medium) .fontWeight(.medium)
.foregroundColor(.secondary) .foregroundColor(.secondary)
.lineLimit(1) .lineLimit(1)
if(fullItem.OfficialRating != "") { if fullItem.OfficialRating != "" {
Text(fullItem.OfficialRating).font(.subheadline) Text(fullItem.OfficialRating).font(.subheadline)
.fontWeight(.semibold) .fontWeight(.semibold)
.foregroundColor(.secondary) .foregroundColor(.secondary)
.lineLimit(1) .lineLimit(1)
.padding(EdgeInsets(top: 1, leading: 4, bottom: 1, trailing: 4)) .padding(EdgeInsets(top: 1, leading: 4, bottom: 1, trailing: 4))
.overlay( .overlay(RoundedRectangle(cornerRadius: 2)
RoundedRectangle(cornerRadius: 2) .stroke(Color.secondary, lineWidth: 1))
.stroke(Color.secondary, lineWidth: 1)
)
} }
if(fullItem.CommunityRating != "0") { if fullItem.CommunityRating != "0" {
HStack() { HStack {
Image(systemName: "star").foregroundColor(.secondary) Image(systemName: "star").foregroundColor(.secondary)
Text(fullItem.CommunityRating).font(.subheadline) Text(fullItem.CommunityRating).font(.subheadline)
.fontWeight(.semibold) .fontWeight(.semibold)
@ -460,59 +523,79 @@ struct EpisodeItemView: View {
} }
Spacer() Spacer()
}.frame(maxWidth: .infinity) }.frame(maxWidth: .infinity)
.offset(x: 14) .offset(x: 14)
}.frame(maxWidth: .infinity) }.frame(maxWidth: .infinity)
Spacer() Spacer()
HStack() { HStack {
Button() { Button {
favorite.toggle() favorite.toggle()
} label: { } label: {
if(!favorite) { if !favorite {
Image(systemName: "heart").foregroundColor(Color.primary).font(.system(size: 20)) Image(systemName: "heart").foregroundColor(Color.primary)
.font(.system(size: 20))
} else { } else {
Image(systemName: "heart.fill").foregroundColor(Color(UIColor.systemRed)).font(.system(size: 20)) Image(systemName: "heart.fill").foregroundColor(Color(UIColor.systemRed))
.font(.system(size: 20))
} }
} }
Button() { Button {
watched.toggle() watched.toggle()
} label: { } label: {
if(watched) { if watched {
Image(systemName: "checkmark.rectangle.fill").foregroundColor(Color.primary).font(.system(size: 20)) Image(systemName: "checkmark.rectangle.fill").foregroundColor(Color.primary)
.font(.system(size: 20))
} else { } else {
Image(systemName: "xmark.rectangle").foregroundColor(Color.primary).font(.system(size: 20)) Image(systemName: "xmark.rectangle").foregroundColor(Color.primary)
.font(.system(size: 20))
} }
} }
} }
}.padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) }.padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55)
if(fullItem.Tagline != "") { if fullItem.Tagline != "" {
Text(fullItem.Tagline).font(.body).italic().padding(.top, 3).fixedSize(horizontal: false, vertical: true).padding(.leading, 16).padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) Text(fullItem.Tagline).font(.body).italic().padding(.top, 3)
.fixedSize(horizontal: false, vertical: true).padding(.leading, 16)
.padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55)
} }
Text(fullItem.Overview).font(.footnote).padding(.top, 3).fixedSize(horizontal: false, vertical: true).padding(.bottom, 3).padding(.leading, 16).padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) Text(fullItem.Overview).font(.footnote).padding(.top, 3)
if(fullItem.Genres.count != 0) { .fixedSize(horizontal: false, vertical: true).padding(.bottom, 3).padding(.leading, 16)
.padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55)
if !fullItem.Genres.isEmpty {
ScrollView(.horizontal, showsIndicators: false) { ScrollView(.horizontal, showsIndicators: false) {
HStack() { HStack {
Text("Genres:").font(.callout).fontWeight(.semibold) Text("Genres:").font(.callout).fontWeight(.semibold)
ForEach(fullItem.Genres, id: \.Id) {genre in ForEach(fullItem.Genres, id: \.Id) { genre in
// NavigationLink(destination: LibraryView(extraParams: "&Genres=\(genre.Name.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "")", title: genre.Name)) { NavigationLink(destination: LibraryView(viewModel: .init(filter: Filter(genres: [
genre
.Name,
])), title: genre.Name)) {
Text(genre.Name).font(.footnote) Text(genre.Name).font(.footnote)
// } }
} }
}.padding(.leading, 16).padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) }.padding(.leading, 16)
.padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55)
} }
} }
if(fullItem.Cast.count != 0) { if !fullItem.Cast.isEmpty {
ScrollView(.horizontal, showsIndicators: false) { ScrollView(.horizontal, showsIndicators: false) {
VStack() { VStack {
Spacer().frame(height: 8); Spacer().frame(height: 8)
HStack() { HStack {
Spacer().frame(width: 16) Spacer().frame(width: 16)
ForEach(fullItem.Cast, id: \.Id) { cast in ForEach(fullItem.Cast, id: \.Id) { cast in
// NavigationLink(destination: LibraryView(extraParams: "&PersonIds=\(cast.Id)", title: cast.Name)) { NavigationLink(destination: LibraryView(viewModel: .init(filter: Filter(personIds: [
VStack() { cast
.Id,
])), title: cast.Name)) {
VStack {
WebImage(url: cast.Image) WebImage(url: cast.Image)
.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: (cast.ImageBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : cast.ImageBlurHash), size: CGSize(width: 16, height: 16))!) Image(uiImage: UIImage(blurHash: cast
.ImageBlurHash == "" ?
"W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" :
cast.ImageBlurHash,
size: CGSize(width: 16,
height: 16))!)
.resizable() .resizable()
.aspectRatio(contentMode: .fill) .aspectRatio(contentMode: .fill)
.frame(width: 100, height: 100) .frame(width: 100, height: 100)
@ -521,12 +604,14 @@ struct EpisodeItemView: View {
.aspectRatio(contentMode: .fill) .aspectRatio(contentMode: .fill)
.frame(width: 100, height: 100) .frame(width: 100, height: 100)
.cornerRadius(10) .cornerRadius(10)
Text(cast.Name).font(.footnote).fontWeight(.regular).lineLimit(1).frame(width: 100).foregroundColor(Color.primary) Text(cast.Name).font(.footnote).fontWeight(.regular).lineLimit(1)
if(cast.Role != "") { .frame(width: 100).foregroundColor(Color.primary)
Text(cast.Role).font(.caption).fontWeight(.medium).lineLimit(1).foregroundColor(Color.secondary).frame(width: 100) if cast.Role != "" {
Text(cast.Role).font(.caption).fontWeight(.medium).lineLimit(1)
.foregroundColor(Color.secondary).frame(width: 100)
} }
} }
// } }
Spacer().frame(width: 10) Spacer().frame(width: 10)
} }
Spacer().frame(width: UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) Spacer().frame(width: UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55)
@ -534,28 +619,35 @@ struct EpisodeItemView: View {
} }
}.padding(.top, -3) }.padding(.top, -3)
} }
if(fullItem.Directors.count != 0) { if !fullItem.Directors.isEmpty {
HStack() { HStack {
Text("Directors:").font(.callout).fontWeight(.semibold) Text("Directors:").font(.callout).fontWeight(.semibold)
Text(fullItem.Directors.joined(separator: ", ")).font(.footnote).lineLimit(1).foregroundColor(Color.secondary) Text(fullItem.Directors.joined(separator: ", ")).font(.footnote).lineLimit(1)
}.padding(.leading, 16).padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) .foregroundColor(Color.secondary)
}.padding(.leading, 16)
.padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55)
} }
if(fullItem.Writers.count != 0) { if !fullItem.Writers.isEmpty {
HStack() { HStack {
Text("Writers:").font(.callout).fontWeight(.semibold) Text("Writers:").font(.callout).fontWeight(.semibold)
Text(fullItem.Writers.joined(separator: ", ")).font(.footnote).lineLimit(1).foregroundColor(Color.secondary) Text(fullItem.Writers.joined(separator: ", ")).font(.footnote).lineLimit(1)
}.padding(.leading, 16).padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) .foregroundColor(Color.secondary)
}.padding(.leading, 16)
.padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55)
} }
if(fullItem.Studios.count != 0) { if !fullItem.Studios.isEmpty {
HStack() { HStack {
Text("Studios:").font(.callout).fontWeight(.semibold) Text("Studios:").font(.callout).fontWeight(.semibold)
Text(fullItem.Studios.joined(separator: ", ")).font(.footnote).lineLimit(1).foregroundColor(Color.secondary) Text(fullItem.Studios.joined(separator: ", ")).font(.footnote).lineLimit(1)
}.padding(.leading, 16).padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) .foregroundColor(Color.secondary)
}.padding(.leading, 16)
.padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55)
} }
Spacer().frame(height: 100); Spacer().frame(height: 100)
}.frame(maxHeight: .infinity) }.frame(maxHeight: .infinity)
} }
}.padding(.top, 16).padding(.leading, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55).edgesIgnoringSafeArea(.leading) }.padding(.top, 16).padding(.leading, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55)
.edgesIgnoringSafeArea(.leading)
} }
} }
} }
@ -564,9 +656,9 @@ struct EpisodeItemView: View {
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.navigationTitle("\(fullItem.Name) - S\(String(fullItem.ParentIndexNumber ?? 0)):E\(String(fullItem.IndexNumber ?? 0)) - \(fullItem.SeriesName ?? "")") .navigationTitle("\(fullItem.Name) - S\(String(fullItem.ParentIndexNumber ?? 0)):E\(String(fullItem.IndexNumber ?? 0)) - \(fullItem.SeriesName ?? "")")
}.onAppear(perform: loadData) }.onAppear(perform: loadData)
.supportedOrientations(.allButUpsideDown) .supportedOrientations(.allButUpsideDown)
.overrideViewPreference(.unspecified) .overrideViewPreference(.unspecified)
.preferredColorScheme(.none) .preferredColorScheme(.none)
.prefersHomeIndicatorAutoHidden(false) .prefersHomeIndicatorAutoHidden(false)
} }
} }

View File

@ -53,25 +53,25 @@ struct ServerAuthByNameResponse: Codable {
var AccessToken: String var AccessToken: String
} }
class ResumeItem: ObservableObject { struct ResumeItem {
@Published var Name: String = ""; var Name: String = "";
@Published var Id: String = ""; var Id: String = "";
@Published var IndexNumber: Int? = nil; var IndexNumber: Int? = nil;
@Published var ParentIndexNumber: Int? = nil; var ParentIndexNumber: Int? = nil;
@Published var Image: String = ""; var Image: String = "";
@Published var ImageType: String = ""; var ImageType: String = "";
@Published var BlurHash: String = ""; var BlurHash: String = "";
@Published var `Type`: String = ""; var `Type`: String = "";
@Published var SeasonId: String? = nil; var SeasonId: String? = nil;
@Published var SeriesId: String? = nil; var SeriesId: String? = nil;
@Published var SeriesName: String? = nil; var SeriesName: String? = nil;
@Published var ItemProgress: Double = 0; var ItemProgress: Double = 0;
@Published var SeasonImage: String? = nil; var SeasonImage: String? = nil;
@Published var SeasonImageType: String? = nil; var SeasonImageType: String? = nil;
@Published var SeasonImageBlurHash: String? = nil; var SeasonImageBlurHash: String? = nil;
@Published var ItemBadge: Int? = 0; var ItemBadge: Int? = 0;
@Published var ProductionYear: Int = 1999; var ProductionYear: Int = 1999;
@Published var Watched: Bool = false; var Watched: Bool = false;
} }
struct ServerMeResponse: Codable { struct ServerMeResponse: Codable {

View File

@ -47,7 +47,7 @@ struct LatestMediaView: View {
let json = try JSON(data: body) let json = try JSON(data: body)
for (_,item):(String, JSON) in json { for (_,item):(String, JSON) in json {
// Do something you want // Do something you want
let itemObj = ResumeItem() var itemObj = ResumeItem()
itemObj.Image = item["ImageTags"]["Primary"].string ?? "" itemObj.Image = item["ImageTags"]["Primary"].string ?? ""
itemObj.ImageType = "Primary" itemObj.ImageType = "Primary"
itemObj.BlurHash = item["ImageBlurHashes"]["Primary"][itemObj.Image].string ?? "" itemObj.BlurHash = item["ImageBlurHashes"]["Primary"][itemObj.Image].string ?? ""

View File

@ -14,173 +14,146 @@ struct Genre: Hashable, Identifiable {
var id: String { name } var id: String { name }
} }
struct LibraryFilterView: View { struct LibraryFilterView: View {
@Environment(\.managedObjectContext) private var viewContext @Environment(\.presentationMode)
@EnvironmentObject var globalData: GlobalData var presentationMode
@Environment(\.managedObjectContext)
@State var library: String; private var viewContext
@Binding var output: String; @EnvironmentObject
@State private var isLoading: Bool = true; var globalData: GlobalData
@State private var onlyUnplayed: Bool = false;
@State private var allGenres: [Genre] = []; @State
@State private var selectedGenres: Set<Genre> = []; var library: String
@State private var allRatings: [Genre] = [];
@State private var selectedRatings: Set<Genre> = [];
@State private var sortBySelection: String = "SortName";
@State private var sortOrder: String = "Descending";
@State private var viewDidLoad: Bool = false;
@Binding var close: Bool;
@Binding
var filter: Filter
@State
private var isLoading: Bool = true
@State
private var onlyUnplayed: Bool = false
@State
private var allGenres: [Genre] = []
@State
private var selectedGenres: Set<Genre> = []
@State
private var allRatings: [Genre] = []
@State
private var selectedRatings: Set<Genre> = []
@State
private var sortBySelection: String = "SortName"
@State
private var sortOrder: String = "Descending"
@State
private var viewDidLoad: Bool = false
func onAppear() { func onAppear() {
if(_viewDidLoad.wrappedValue == true) { if _viewDidLoad.wrappedValue == true {
return return
} }
_viewDidLoad.wrappedValue = true; _viewDidLoad.wrappedValue = true
if(_output.wrappedValue.contains("&Filters=IsUnplayed")) { if filter.filterTypes.contains(.isUnplayed) {
_onlyUnplayed.wrappedValue = true; _onlyUnplayed.wrappedValue = true
} }
if(_output.wrappedValue.contains("&Genres=")) { if !filter.genres.isEmpty {
let genreString = _output.wrappedValue.components(separatedBy: "&Genres=")[1].components(separatedBy: "&")[0]; _selectedGenres.wrappedValue = Set(filter.genres.map { Genre(name: $0) })
for genre in genreString.components(separatedBy: "%7C") {
_selectedGenres.wrappedValue.insert(Genre(name: genre.removingPercentEncoding ?? ""))
}
} }
if(_output.wrappedValue.contains("&OfficialRatings=")) { if !filter.officialRatings.isEmpty {
let ratingString = _output.wrappedValue.components(separatedBy: "&OfficialRatings=")[1].components(separatedBy: "&")[0]; _selectedRatings.wrappedValue = Set(filter.officialRatings.map { Genre(name: $0) })
for rating in ratingString.components(separatedBy: "%7C") {
_selectedRatings.wrappedValue.insert(Genre(name: rating.removingPercentEncoding ?? ""))
}
} }
let sortBy = _output.wrappedValue.components(separatedBy: "&SortBy=")[1].components(separatedBy: "&")[0]; _sortBySelection.wrappedValue = filter.sort?.rawValue ?? sortBySelection
_sortBySelection.wrappedValue = sortBy; _sortOrder.wrappedValue = filter.asc?.rawValue ?? sortOrder
let sortOrder = _output.wrappedValue.components(separatedBy: "&SortOrder=")[1].components(separatedBy: "&")[0];
_sortOrder.wrappedValue = sortOrder;
recalculateFilters()
_allGenres.wrappedValue = [] _allGenres.wrappedValue = []
let url = "/Items/Filters?UserId=\(globalData.user?.user_id ?? "")&ParentId=\(library)" let url = "/Items/Filters?UserId=\(globalData.user?.user_id ?? "")&ParentId=\(library)"
let request = RestRequest(method: .get, url: (globalData.server?.baseURI ?? "") + url) let request = RestRequest(method: .get, url: (globalData.server?.baseURI ?? "") + url)
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"
request.responseData() { (result: Result<RestResponse<Data>, RestError>) in request.responseData { (result: Result<RestResponse<Data>, RestError>) in
switch result { switch result {
case .success(let response): case let .success(response):
let body = response.body let body = response.body
do { do {
let json = try JSON(data: body) let json = try JSON(data: body)
let arr = json["Genres"].arrayObject as? [String] ?? [] let arr = json["Genres"].arrayObject as? [String] ?? []
for genreName in arr { for genreName in arr {
//print(genreName) // print(genreName)
let genre = Genre(name: genreName) let genre = Genre(name: genreName)
allGenres.append(genre) allGenres.append(genre)
} }
let arr2 = json["OfficialRatings"].arrayObject as? [String] ?? [] let arr2 = json["OfficialRatings"].arrayObject as? [String] ?? []
for genreName in arr2 { for genreName in arr2 {
//print(genreName) // print(genreName)
let genre = Genre(name: genreName) let genre = Genre(name: genreName)
allRatings.append(genre) allRatings.append(genre)
} }
} catch { } catch {}
case let .failure(error):
}
break
case .failure(let error):
debugPrint(error) debugPrint(error)
break
} }
isLoading = false; isLoading = false
} }
} }
func recalculateFilters() {
print("recalcFilters running");
output = "";
if(_onlyUnplayed.wrappedValue) {
output = "&Filters=IsUnPlayed";
}
if(selectedGenres.count != 0) {
output += "&Genres="
var genres: [String] = []
for genre in selectedGenres {
genres.append(genre.name.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "")
}
output += genres.joined(separator: "%7C")
}
if(selectedRatings.count != 0) {
output += "&OfficialRatings="
var genres: [String] = []
for genre in selectedRatings {
genres.append(genre.name.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "")
}
output += genres.joined(separator: "%7C")
}
output += "&SortBy=\(sortBySelection)&SortOrder=\(sortOrder)"
//print(output)
}
var body: some View { var body: some View {
NavigationView() { NavigationView {
LoadingView(isShowing: $isLoading) { LoadingView(isShowing: $isLoading) {
Form { Form {
Toggle("Only show unplayed items", isOn: $onlyUnplayed) Toggle("Only show unplayed items", isOn: $onlyUnplayed)
.onChange(of: onlyUnplayed) { tag in .onChange(of: onlyUnplayed) { value in
recalculateFilters() if value {
filter.filterTypes.append(.isUnplayed)
} else {
filter.filterTypes.removeAll { $0 == .isUnplayed }
}
} }
MultiSelector( MultiSelector(label: "Genres",
label: "Genres", options: allGenres,
options: allGenres, optionToString: { $0.name },
optionToString: { $0.name }, selected: $selectedGenres)
selected: $selectedGenres .onChange(of: selectedGenres) { genres in
).onChange(of: selectedGenres) { tag in filter.genres = genres.map(\.id)
recalculateFilters() }
} MultiSelector(label: "Parental Ratings",
MultiSelector( options: allRatings,
label: "Parental Ratings", optionToString: { $0.name },
options: allRatings, selected: $selectedRatings)
optionToString: { $0.name }, .onChange(of: selectedRatings) { ratings in
selected: $selectedRatings filter.officialRatings = ratings.map(\.id)
).onChange(of: selectedRatings) { tag in }
recalculateFilters()
}
Section(header: Text("Sort settings")) { Section(header: Text("Sort settings")) {
Picker("Sort by", selection: $sortBySelection) { Picker("Sort by", selection: $sortBySelection) {
Text("Name").tag("SortName") Text("Name").tag("SortName")
Text("Date Added").tag("DateCreated") Text("Date Added").tag("DateCreated")
Text("Date Played").tag("DatePlayed") Text("Date Played").tag("DatePlayed")
Text("Date Released").tag("PremiereDate") Text("Date Released").tag("PremiereDate")
Text("Runtime").tag("Runtime") Text("Runtime").tag("Runtime")
}.onChange(of: sortBySelection) { tag in }.onChange(of: sortBySelection) { value in
recalculateFilters() guard let sort = SortType(rawValue: value) else { return }
filter.sort = sort
} }
Picker("Sort order", selection: $sortOrder) { Picker("Sort order", selection: $sortOrder) {
Text("Ascending").tag("Ascending") Text("Ascending").tag("Ascending")
Text("Descending").tag("Descending") Text("Descending").tag("Descending")
}.onChange(of: sortOrder) { tag in }.onChange(of: sortOrder) { order in
recalculateFilters() guard let asc = ASC(rawValue: order) else { return }
filter.asc = asc
} }
} }
} }
}.onAppear(perform: onAppear) }.onAppear(perform: onAppear)
.navigationBarTitle("Filters", displayMode: .inline) .navigationBarTitle("Filters", displayMode: .inline)
.toolbar { .navigationBarItems(leading: Button {
ToolbarItemGroup(placement: .navigationBarLeading) { presentationMode.wrappedValue.dismiss()
Button { } label: {
close = false HStack {
} label: { Text("Back").font(.callout)
HStack() {
Text("Back").font(.callout)
}
} }
} })
}
} }
} }
} }

View File

@ -40,26 +40,25 @@ struct LibraryListView: View {
var body: some View { var body: some View {
List(libraryIDs, id: \.self) { id in List(libraryIDs, id: \.self) { id in
if id != "genres" { switch id {
NavigationLink(destination: LibraryView(viewModel: .init(prefillID: id), title: libraryNames[id] ?? "")) { case "favorites":
NavigationLink(destination: LibraryView(viewModel: .init(filter: Filter(filterTypes: [.isFavorite])),
title: libraryNames[id] ?? "")) {
Text(libraryNames[id] ?? "").foregroundColor(Color.primary) Text(libraryNames[id] ?? "").foregroundColor(Color.primary)
} }
} else { case "genres":
// NavigationLink(destination: LibraryView(prefill: id, names: libraryNames, libraries: library_ids)) {
Text(libraryNames[id] ?? "").foregroundColor(Color.primary) Text(libraryNames[id] ?? "").foregroundColor(Color.primary)
// } default:
NavigationLink(destination: LibraryView(viewModel: .init(filter: Filter(parentID: id)), title: libraryNames[id] ?? "")) {
Text(libraryNames[id] ?? "").foregroundColor(Color.primary)
}
} }
} }
.onAppear(perform: listOnAppear) .onAppear(perform: listOnAppear)
.navigationTitle("All Media") .navigationTitle("All Media")
.toolbar { .navigationBarItems(trailing:
ToolbarItemGroup(placement: .navigationBarTrailing) { NavigationLink(destination: LibrarySearchView(viewModel: .init(filter: .init()))) {
// NavigationLink(destination: LibrarySearchView(viewModel: .init(filter: .init()),
// close: $closeSearch),
// isActive: $closeSearch) {
Image(systemName: "magnifyingglass") Image(systemName: "magnifyingglass")
// } })
}
}
} }
} }

View File

@ -15,31 +15,22 @@ struct LibrarySearchView: View {
private var viewContext private var viewContext
@EnvironmentObject @EnvironmentObject
var globalData: GlobalData var globalData: GlobalData
@ObservedObject @ObservedObject
var viewModel: LibrarySearchViewModel var viewModel: LibrarySearchViewModel
@Binding
var close: Bool
@State @State
var open: Bool = false private var tracks: [GridItem] = []
@State
private var onlyUnplayed: Bool = false
@State
private var viewDidLoad: Bool = false
@State
var linkedItem = ResumeItem()
func onAppear() {
recalcTracks()
viewModel.globalData = globalData
}
@Environment(\.verticalSizeClass) @Environment(\.verticalSizeClass)
var verticalSizeClass: UserInterfaceSizeClass? var verticalSizeClass: UserInterfaceSizeClass?
@Environment(\.horizontalSizeClass) @Environment(\.horizontalSizeClass)
var horizontalSizeClass: UserInterfaceSizeClass? var horizontalSizeClass: UserInterfaceSizeClass?
func onAppear() {
recalcTracks()
viewModel.globalData = globalData
}
var isPortrait: Bool { var isPortrait: Bool {
let result = verticalSizeClass == .regular && horizontalSizeClass == .compact let result = verticalSizeClass == .regular && horizontalSizeClass == .compact
return result return result
@ -53,15 +44,9 @@ struct LibrarySearchView: View {
} }
} }
@State
private var tracks: [GridItem] = []
var body: some View { var body: some View {
ZStack { ZStack {
VStack { VStack {
NavigationLink(destination: ItemView(item: linkedItem), isActive: $open) {
EmptyView()
}
Spacer().frame(height: 6) Spacer().frame(height: 6)
TextField("Search", text: $viewModel.searchQuery, onEditingChanged: { _ in TextField("Search", text: $viewModel.searchQuery, onEditingChanged: { _ in
print("changed") print("changed")
@ -72,11 +57,7 @@ struct LibrarySearchView: View {
ScrollView(.vertical) { ScrollView(.vertical) {
LazyVGrid(columns: tracks) { LazyVGrid(columns: tracks) {
ForEach(viewModel.items, id: \.Id) { item in ForEach(viewModel.items, id: \.Id) { item in
Button { NavigationLink(destination: ItemView(item: item)) {
_linkedItem.wrappedValue = item
_close.wrappedValue = false
_open.wrappedValue = true
} label: {
ResumeItemGridCell(item: item) ResumeItemGridCell(item: item)
} }
} }
@ -87,6 +68,8 @@ struct LibrarySearchView: View {
} }
if viewModel.isLoading { if viewModel.isLoading {
ActivityIndicator($viewModel.isLoading) ActivityIndicator($viewModel.isLoading)
} else if viewModel.items.isEmpty {
Text("Empty Response")
} }
} }
.onAppear(perform: onAppear) .onAppear(perform: onAppear)

View File

@ -18,28 +18,24 @@ struct LibraryView: View {
@ObservedObject @ObservedObject
var viewModel: LibraryViewModel var viewModel: LibraryViewModel
@State
private var viewDidLoad: Bool = false
@State @State
private var showFiltersPopover: Bool = false private var showFiltersPopover: Bool = false
@State @State
private var showSearchPopover: Bool = false private var showSearchPopover: Bool = false
private var title: String
@State @State
private var title: String = "" private var tracks: [GridItem] = []
@State
private var closeSearch: Bool = false
init(viewModel: LibraryViewModel, title: String) { init(viewModel: LibraryViewModel, title: String) {
self.viewModel = viewModel self.viewModel = viewModel
self._title = State(initialValue: title) self.title = title
} }
func onAppear() { func onAppear() {
recalcTracks()
viewModel.globalData = globalData viewModel.globalData = globalData
if viewModel.items.isEmpty {
recalcTracks()
viewModel.requestInitItems()
}
} }
@Environment(\.verticalSizeClass) @Environment(\.verticalSizeClass)
@ -60,11 +56,8 @@ struct LibraryView: View {
} }
} }
@State
private var tracks: [GridItem] = []
var body: some View { var body: some View {
LoadingView(isShowing: $viewModel.isLoading) { ZStack {
ScrollView(.vertical) { ScrollView(.vertical) {
Spacer().frame(height: 16) Spacer().frame(height: 16)
LazyVGrid(columns: tracks) { LazyVGrid(columns: tracks) {
@ -76,51 +69,46 @@ struct LibraryView: View {
} }
Spacer().frame(height: 16) Spacer().frame(height: 16)
} }
.gesture(DragGesture().onChanged { value in
if value.translation.height > 0 {
print("Scroll down")
} else {
print("Scroll up")
}
})
.onChange(of: isPortrait) { _ in .onChange(of: isPortrait) { _ in
recalcTracks() recalcTracks()
} }
if viewModel.isLoading {
ActivityIndicator($viewModel.isLoading)
} else if viewModel.items.isEmpty {
Text("Empty Response")
}
} }
.overrideViewPreference(.unspecified) .overrideViewPreference(.unspecified)
.onAppear(perform: onAppear) .onAppear(perform: onAppear)
.navigationTitle(title) .navigationTitle(title)
.toolbar { .navigationBarItems(trailing: HStack {
ToolbarItemGroup(placement: .navigationBarTrailing) { if !viewModel.isHiddenPreviousButton {
if !viewModel.isHiddenPreviousButton {
Button {
viewModel.requestPreviousPage()
} label: {
Image(systemName: "chevron.left")
}
}
if !viewModel.isHiddenNextButton {
Button {
viewModel.requestNextPage()
} label: {
Image(systemName: "chevron.right")
}
}
NavigationLink(destination: LibrarySearchView(viewModel: .init(filter: viewModel.filter), close: $closeSearch),
isActive: $closeSearch) {
Image(systemName: "magnifyingglass")
}
Button { Button {
showFiltersPopover = true viewModel.requestPreviousPage()
} label: { } label: {
Image(systemName: "line.horizontal.3.decrease") Image(systemName: "chevron.left")
} }
} }
if !viewModel.isHiddenNextButton {
Button {
viewModel.requestNextPage()
} label: {
Image(systemName: "chevron.right")
}
}
NavigationLink(destination: LibrarySearchView(viewModel: .init(filter: viewModel.filter))) {
Image(systemName: "magnifyingglass")
}
Button {
showFiltersPopover = true
} label: {
Image(systemName: "line.horizontal.3.decrease")
}
})
.sheet(isPresented: self.$showFiltersPopover) {
LibraryFilterView(library: viewModel.filter.parentID ?? "", filter: $viewModel.filter)
.environmentObject(self.globalData)
} }
// .sheet(isPresented: self.$showFiltersPopover) {
// LibraryFilterView(library: selected_library_id, output: $filterString, close: $showFiltersPopover)
// .environmentObject(self.globalData)
// }
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -39,7 +39,7 @@ struct NextUpView: View {
let json = try JSON(data: body) let json = try JSON(data: body)
for (_,item):(String, JSON) in json["Items"] { for (_,item):(String, JSON) in json["Items"] {
// Do something you want // Do something you want
let itemObj = ResumeItem() var itemObj = ResumeItem()
itemObj.Image = item["SeriesPrimaryImageTag"].string ?? "" itemObj.Image = item["SeriesPrimaryImageTag"].string ?? ""
itemObj.ImageType = "Primary" itemObj.ImageType = "Primary"
itemObj.BlurHash = item["ImageBlurHashes"]["Primary"][itemObj.Image].string ?? "" itemObj.BlurHash = item["ImageBlurHashes"]["Primary"][itemObj.Image].string ?? ""

View File

@ -113,7 +113,7 @@ struct SeasonItemView: View {
episode.ParentId = episode.SeasonId ?? ""; episode.ParentId = episode.SeasonId ?? "";
episode.CommunityRating = String(json["CommunityRating"].float ?? 0.0) episode.CommunityRating = String(json["CommunityRating"].float ?? 0.0)
let rI = ResumeItem() var rI = ResumeItem()
rI.Name = episode.Name; rI.Name = episode.Name;
rI.Id = episode.Id; rI.Id = episode.Id;
rI.IndexNumber = episode.IndexNumber; rI.IndexNumber = episode.IndexNumber;

View File

@ -37,7 +37,7 @@ struct SeriesItemView: View {
let json = try JSON(data: body) let json = try JSON(data: body)
for (_,item):(String, JSON) in json["Items"] { for (_,item):(String, JSON) in json["Items"] {
// Do something you want // Do something you want
let itemObj = ResumeItem() var itemObj = ResumeItem()
itemObj.Type = "Season" itemObj.Type = "Season"
itemObj.Id = item["Id"].string ?? "" itemObj.Id = item["Id"].string ?? ""
itemObj.ProductionYear = item["ProductionYear"].int ?? 0 itemObj.ProductionYear = item["ProductionYear"].int ?? 0

View File

@ -101,17 +101,14 @@ struct SettingsView: View {
} }
.navigationBarTitle("Settings", displayMode: .inline) .navigationBarTitle("Settings", displayMode: .inline)
.toolbar { .navigationBarItems(leading:
ToolbarItemGroup(placement: .navigationBarLeading) { Button {
Button { close = false
close = false } label: {
} label: { HStack() {
HStack() { Text("Back").font(.callout)
Text("Back").font(.callout) }
} })
}
}
}
}.onAppear(perform: onAppear) }.onAppear(perform: onAppear)
} }
} }