add MovieItemViewModel

This commit is contained in:
PangMo5 2021-06-19 05:53:29 +09:00
parent 0265f46171
commit 7267b37cb7
4 changed files with 144 additions and 114 deletions

View File

@ -123,6 +123,8 @@
62E632DE267D2E170063E547 /* LatestMediaViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632D9267D2BC40063E547 /* LatestMediaViewModel.swift */; };
62E632E0267D30CA0063E547 /* LibraryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632DF267D30CA0063E547 /* LibraryViewModel.swift */; };
62E632E1267D30CA0063E547 /* LibraryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632DF267D30CA0063E547 /* LibraryViewModel.swift */; };
62E632E3267D3BA60063E547 /* MovieItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632E2267D3BA60063E547 /* MovieItemViewModel.swift */; };
62E632E4267D3BA60063E547 /* MovieItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632E2267D3BA60063E547 /* MovieItemViewModel.swift */; };
62EC3527267665D8000E9F2D /* MobileVLCKit.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 53D5E3DC264B47EE00BADDC8 /* MobileVLCKit.xcframework */; };
62EC3528267665D8000E9F2D /* MobileVLCKit.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 53D5E3DC264B47EE00BADDC8 /* MobileVLCKit.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
62EC352C26766675000E9F2D /* ServerEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EC352B26766675000E9F2D /* ServerEnvironment.swift */; };
@ -274,6 +276,7 @@
62E632D9267D2BC40063E547 /* LatestMediaViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LatestMediaViewModel.swift; sourceTree = "<group>"; };
62E632DB267D2E130063E547 /* LibrarySearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibrarySearchViewModel.swift; sourceTree = "<group>"; };
62E632DF267D30CA0063E547 /* LibraryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryViewModel.swift; sourceTree = "<group>"; };
62E632E2267D3BA60063E547 /* MovieItemViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieItemViewModel.swift; sourceTree = "<group>"; };
62EC352B26766675000E9F2D /* ServerEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerEnvironment.swift; sourceTree = "<group>"; };
62EC352E267666A5000E9F2D /* SessionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionManager.swift; sourceTree = "<group>"; };
62EC353326766B03000E9F2D /* DeviceRotationViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceRotationViewModifier.swift; sourceTree = "<group>"; };
@ -338,6 +341,7 @@
62E632D9267D2BC40063E547 /* LatestMediaViewModel.swift */,
62E632DB267D2E130063E547 /* LibrarySearchViewModel.swift */,
62E632DF267D30CA0063E547 /* LibraryViewModel.swift */,
62E632E2267D3BA60063E547 /* MovieItemViewModel.swift */,
);
path = ViewModels;
sourceTree = "<group>";
@ -722,6 +726,7 @@
531690E5267ABD5C005D8AB9 /* MainTabView.swift in Sources */,
53ABFDE7267974EF00886593 /* ConnectToServerViewModel.swift in Sources */,
53ABFDEE26799DCD00886593 /* ImageView.swift in Sources */,
62E632E4267D3BA60063E547 /* MovieItemViewModel.swift in Sources */,
5358706C2669D21700D05A09 /* PersistenceController.swift in Sources */,
535870AA2669D8AE00D05A09 /* BlurHashDecode.swift in Sources */,
53ABFDE5267974EF00886593 /* ViewModel.swift in Sources */,
@ -776,6 +781,7 @@
531AC8BF26750DE20091C7EB /* ImageView.swift in Sources */,
62E632E0267D30CA0063E547 /* LibraryViewModel.swift in Sources */,
62EC352F267666A5000E9F2D /* SessionManager.swift in Sources */,
62E632E3267D3BA60063E547 /* MovieItemViewModel.swift in Sources */,
535870AD2669D8DD00D05A09 /* Typings.swift in Sources */,
62EC352C26766675000E9F2D /* ServerEnvironment.swift in Sources */,
6267B3DA2671138200A7371D /* ImageExtensions.swift in Sources */,

View File

@ -39,7 +39,7 @@ struct ItemView: View {
}
VStack {
if item.type == "Movie" {
MovieItemView(item: item)
MovieItemView(viewModel: .init(item: item))
} else if item.type == "Season" {
SeasonItemView(item: item)
} else if item.type == "Series" {

View File

@ -11,7 +11,7 @@ import SwiftUI
struct MovieItemView: View {
@StateObject
var tempViewModel = ViewModel()
var viewModel: MovieItemViewModel
@State
private var orientation = UIDeviceOrientation.unknown
@Environment(\.horizontalSizeClass)
@ -21,60 +21,9 @@ struct MovieItemView: View {
@EnvironmentObject
private var playbackInfo: VideoPlayerItem
var item: BaseItemDto
@State
private var settingState: Bool = true
@State
private var watched: Bool = false {
didSet {
if !settingState {
if watched == true {
PlaystateAPI.markPlayedItem(userId: SessionManager.current.user.user_id!, itemId: item.id!)
.sink(receiveCompletion: { completion in
print(completion)
}, receiveValue: { _ in
})
.store(in: &tempViewModel.cancellables)
} else {
PlaystateAPI.markUnplayedItem(userId: SessionManager.current.user.user_id!, itemId: item.id!)
.sink(receiveCompletion: { completion in
print(completion)
}, receiveValue: { _ in
})
.store(in: &tempViewModel.cancellables)
}
}
}
}
@State
private var favorite: Bool = false {
didSet {
if !settingState {
if favorite == true {
UserLibraryAPI.markFavoriteItem(userId: SessionManager.current.user.user_id!, itemId: item.id!)
.sink(receiveCompletion: { completion in
print(completion)
}, receiveValue: { _ in
})
.store(in: &tempViewModel.cancellables)
} else {
UserLibraryAPI.unmarkFavoriteItem(userId: SessionManager.current.user.user_id!, itemId: item.id!)
.sink(receiveCompletion: { completion in
print(completion)
}, receiveValue: { _ in
})
.store(in: &tempViewModel.cancellables)
}
}
}
}
var portraitHeaderView: some View {
ImageView(src: item
.getBackdropImage(maxWidth: UIDevice.current.userInterfaceIdiom == .pad ? 622 : Int(UIScreen.main.bounds.width)),
bh: item.getBackdropImageBlurHash())
ImageView(src: viewModel.item.getBackdropImage(maxWidth: UIDevice.current.userInterfaceIdiom == .pad ? 622 : Int(UIScreen.main.bounds.width)),
bh: viewModel.item.getBackdropImageBlurHash())
.opacity(0.4)
.blur(radius: 2.0)
}
@ -82,29 +31,29 @@ struct MovieItemView: View {
var portraitHeaderOverlayView: some View {
VStack(alignment: .leading) {
HStack(alignment: .bottom, spacing: 12) {
ImageView(src: item.getPrimaryImage(maxWidth: 120))
ImageView(src: viewModel.item.getPrimaryImage(maxWidth: 120))
.frame(width: 120, height: 180)
.cornerRadius(10)
VStack(alignment: .leading) {
Spacer()
Text(item.name ?? "").font(.headline)
Text(viewModel.item.name ?? "").font(.headline)
.fontWeight(.semibold)
.foregroundColor(.primary)
.lineLimit(1)
.offset(y: 5)
HStack {
if item.productionYear != nil {
Text(String(item.productionYear ?? 0)).font(.subheadline)
if viewModel.item.productionYear != nil {
Text(String(viewModel.item.productionYear ?? 0)).font(.subheadline)
.fontWeight(.medium)
.foregroundColor(.secondary)
.lineLimit(1)
}
Text(item.getItemRuntime()).font(.subheadline)
Text(viewModel.item.getItemRuntime()).font(.subheadline)
.fontWeight(.medium)
.foregroundColor(.secondary)
.lineLimit(1)
if item.officialRating != nil {
Text(item.officialRating!).font(.subheadline)
if viewModel.item.officialRating != nil {
Text(viewModel.item.officialRating!).font(.subheadline)
.fontWeight(.semibold)
.foregroundColor(.secondary)
.lineLimit(1)
@ -119,11 +68,11 @@ struct MovieItemView: View {
HStack {
// Play button
Button {
self.playbackInfo.itemToPlay = item
self.playbackInfo.itemToPlay = viewModel.item
self.playbackInfo.shouldShowPlayer = true
} label: {
HStack {
Text(item.getItemProgressString() == "" ? "Play" : "\(item.getItemProgressString()) left")
Text(viewModel.item.getItemProgressString() == "" ? "Play" : "\(viewModel.item.getItemProgressString()) left")
.foregroundColor(Color.white).font(.callout).fontWeight(.semibold)
Image(systemName: "play.fill").foregroundColor(Color.white).font(.system(size: 20))
}
@ -135,19 +84,21 @@ struct MovieItemView: View {
Spacer()
HStack {
Button {
favorite.toggle()
viewModel.updateFavoriteState()
} label: {
if !favorite {
Image(systemName: "heart").foregroundColor(Color.primary).font(.system(size: 20))
} else {
if viewModel.isFavorited {
Image(systemName: "heart.fill").foregroundColor(Color(UIColor.systemRed))
.font(.system(size: 20))
} else {
Image(systemName: "heart").foregroundColor(Color.primary)
.font(.system(size: 20))
}
}
.disabled(viewModel.isLoading)
Button {
watched.toggle()
viewModel.updateWatchState()
} label: {
if watched {
if viewModel.isWatched {
Image(systemName: "checkmark.rectangle.fill").foregroundColor(Color.primary)
.font(.system(size: 20))
} else {
@ -155,6 +106,7 @@ struct MovieItemView: View {
.font(.system(size: 20))
}
}
.disabled(viewModel.isLoading)
}
}.padding(.top, 8)
}
@ -173,19 +125,19 @@ struct MovieItemView: View {
Spacer()
.frame(height: UIDevice.current.userInterfaceIdiom == .pad ? 135 : 40)
.padding(.bottom, UIDevice.current.userInterfaceIdiom == .pad ? 54 : 24)
if !(item.taglines ?? []).isEmpty {
Text(item.taglines!.first!).font(.body).italic().padding(.top, 7)
if !(viewModel.item.taglines ?? []).isEmpty {
Text(viewModel.item.taglines!.first!).font(.body).italic().padding(.top, 7)
.fixedSize(horizontal: false, vertical: true).padding(.leading, 16)
.padding(.trailing, 16)
}
Text(item.overview ?? "").font(.footnote).padding(.top, 3)
Text(viewModel.item.overview ?? "").font(.footnote).padding(.top, 3)
.fixedSize(horizontal: false, vertical: true).padding(.bottom, 3).padding(.leading, 16)
.padding(.trailing, 16)
if !(item.genreItems ?? []).isEmpty {
if !(viewModel.item.genreItems ?? []).isEmpty {
ScrollView(.horizontal, showsIndicators: false) {
HStack {
Text("Genres:").font(.callout).fontWeight(.semibold)
ForEach(item.genreItems!, id: \.id) { genre in
ForEach(viewModel.item.genreItems!, id: \.id) { genre in
NavigationLink(destination: LazyView {
LibraryView(viewModel: .init(genre: genre), title: genre.name ?? "")
}) {
@ -195,13 +147,13 @@ struct MovieItemView: View {
}.padding(.leading, 16).padding(.trailing, 16)
}
}
if !(item.people ?? []).isEmpty {
if !(viewModel.item.people ?? []).isEmpty {
ScrollView(.horizontal, showsIndicators: false) {
VStack {
Spacer().frame(height: 8)
HStack {
Spacer().frame(width: 16)
ForEach(item.people!, id: \.self) { person in
ForEach(viewModel.item.people!, id: \.self) { person in
if person.type! == "Actor" {
NavigationLink(destination: LazyView {
LibraryView(viewModel: .init(person: person), title: person.name ?? "")
@ -228,11 +180,11 @@ struct MovieItemView: View {
}
}.padding(.top, -3)
}
if !(item.studios ?? []).isEmpty {
if !(viewModel.item.studios ?? []).isEmpty {
ScrollView(.horizontal, showsIndicators: false) {
HStack {
Text("Studios:").font(.callout).fontWeight(.semibold)
ForEach(item.studios!, id: \.id) { studio in
ForEach(viewModel.item.studios!, id: \.id) { studio in
NavigationLink(destination: LazyView {
LibraryView(viewModel: .init(studio: studio), title: studio.name ?? "")
@ -249,8 +201,8 @@ struct MovieItemView: View {
} else {
GeometryReader { geometry in
ZStack {
ImageView(src: item.getBackdropImage(maxWidth: 200),
bh: item.getBackdropImageBlurHash())
ImageView(src: viewModel.item.getBackdropImage(maxWidth: 200),
bh: viewModel.item.getBackdropImageBlurHash())
.opacity(0.3)
.frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing,
height: geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom)
@ -258,17 +210,17 @@ struct MovieItemView: View {
.blur(radius: 4)
HStack {
VStack {
ImageView(src: item.getPrimaryImage(maxWidth: 120),
bh: item.getPrimaryImageBlurHash())
ImageView(src: viewModel.item.getPrimaryImage(maxWidth: 120),
bh: viewModel.item.getPrimaryImageBlurHash())
.frame(width: 120, height: 180)
.cornerRadius(10)
Spacer().frame(height: 15)
Button {
self.playbackInfo.itemToPlay = item
self.playbackInfo.itemToPlay = viewModel.item
self.playbackInfo.shouldShowPlayer = true
} label: {
HStack {
Text(item.getItemProgressString() == "" ? "Play" : "\(item.getItemProgressString()) left")
Text(viewModel.item.getItemProgressString() == "" ? "Play" : "\(viewModel.item.getItemProgressString()) left")
.foregroundColor(Color.white).font(.callout).fontWeight(.semibold)
Image(systemName: "play.fill").foregroundColor(Color.white).font(.system(size: 20))
}
@ -283,25 +235,25 @@ struct MovieItemView: View {
VStack(alignment: .leading) {
HStack {
VStack(alignment: .leading) {
Text(item.name ?? "").font(.headline)
Text(viewModel.item.name ?? "").font(.headline)
.fontWeight(.semibold)
.foregroundColor(.primary)
.fixedSize(horizontal: false, vertical: true)
.offset(x: 14, y: 0)
Spacer().frame(height: 1)
HStack {
if item.productionYear != nil {
Text(String(item.productionYear ?? 0)).font(.subheadline)
if viewModel.item.productionYear != nil {
Text(String(viewModel.item.productionYear ?? 0)).font(.subheadline)
.fontWeight(.medium)
.foregroundColor(.secondary)
.lineLimit(1)
}
Text(item.getItemRuntime()).font(.subheadline)
Text(viewModel.item.getItemRuntime()).font(.subheadline)
.fontWeight(.medium)
.foregroundColor(.secondary)
.lineLimit(1)
if item.officialRating != nil {
Text(item.officialRating!).font(.subheadline)
if viewModel.item.officialRating != nil {
Text(viewModel.item.officialRating!).font(.subheadline)
.fontWeight(.semibold)
.foregroundColor(.secondary)
.lineLimit(1)
@ -309,10 +261,10 @@ struct MovieItemView: View {
.overlay(RoundedRectangle(cornerRadius: 2)
.stroke(Color.secondary, lineWidth: 1))
}
if item.communityRating != nil {
if viewModel.item.communityRating != nil {
HStack {
Image(systemName: "star").foregroundColor(.secondary)
Text(String(item.communityRating!)).font(.subheadline)
Text(String(viewModel.item.communityRating!)).font(.subheadline)
.fontWeight(.semibold)
.foregroundColor(.secondary)
.lineLimit(1)
@ -326,20 +278,21 @@ struct MovieItemView: View {
Spacer()
HStack {
Button {
favorite.toggle()
viewModel.updateFavoriteState()
} label: {
if !favorite {
Image(systemName: "heart").foregroundColor(Color.primary)
if viewModel.isFavorited {
Image(systemName: "heart.fill").foregroundColor(Color(UIColor.systemRed))
.font(.system(size: 20))
} else {
Image(systemName: "heart.fill").foregroundColor(Color(UIColor.systemRed))
Image(systemName: "heart").foregroundColor(Color.primary)
.font(.system(size: 20))
}
}
.disabled(viewModel.isLoading)
Button {
watched.toggle()
viewModel.updateWatchState()
} label: {
if watched {
if viewModel.isWatched {
Image(systemName: "checkmark.rectangle.fill").foregroundColor(Color.primary)
.font(.system(size: 20))
} else {
@ -347,21 +300,22 @@ struct MovieItemView: View {
.font(.system(size: 20))
}
}
.disabled(viewModel.isLoading)
}
}.padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55)
if !(item.taglines ?? []).isEmpty {
Text(item.taglines!.first!).font(.body).italic().padding(.top, 3)
if !(viewModel.item.taglines ?? []).isEmpty {
Text(viewModel.item.taglines!.first!).font(.body).italic().padding(.top, 3)
.fixedSize(horizontal: false, vertical: true).padding(.leading, 16)
.padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55)
}
Text(item.overview ?? "").font(.footnote).padding(.top, 3)
Text(viewModel.item.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)
if !(item.genreItems ?? []).isEmpty {
if !(viewModel.item.genreItems ?? []).isEmpty {
ScrollView(.horizontal, showsIndicators: false) {
HStack {
Text("Genres:").font(.callout).fontWeight(.semibold)
ForEach(item.genreItems!, id: \.id) { genre in
ForEach(viewModel.item.genreItems!, id: \.id) { genre in
NavigationLink(destination: LazyView {
LibraryView(viewModel: .init(genre: genre), title: genre.name ?? "")
}) {
@ -373,13 +327,13 @@ struct MovieItemView: View {
.padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55)
}
}
if !(item.people ?? []).isEmpty {
if !(viewModel.item.people ?? []).isEmpty {
ScrollView(.horizontal, showsIndicators: false) {
VStack {
Spacer().frame(height: 8)
HStack {
Spacer().frame(width: 16)
ForEach(item.people!, id: \.self) { person in
ForEach(viewModel.item.people!, id: \.self) { person in
if person.type! == "Actor" {
NavigationLink(destination: LazyView {
LibraryView(viewModel: .init(person: person), title: person.name ?? "")
@ -408,11 +362,11 @@ struct MovieItemView: View {
}
}.padding(.top, -3)
}
if !(item.studios ?? []).isEmpty {
if !(viewModel.item.studios ?? []).isEmpty {
ScrollView(.horizontal, showsIndicators: false) {
HStack {
Text("Studios:").font(.callout).fontWeight(.semibold)
ForEach(item.studios!, id: \.id) { studio in
ForEach(viewModel.item.studios!, id: \.id) { studio in
NavigationLink(destination: LazyView {
LibraryView(viewModel: .init(studio: studio), title: studio.name ?? "")
}) {
@ -433,15 +387,10 @@ struct MovieItemView: View {
}
}
}
.onAppear(perform: {
favorite = item.userData?.isFavorite ?? false
watched = item.userData?.played ?? false
settingState = false
})
.onRotate {
orientation = $0
}
.navigationBarTitleDisplayMode(.inline)
.navigationTitle(item.name ?? "")
.navigationTitle(viewModel.item.name ?? "")
}
}

View File

@ -0,0 +1,75 @@
//
/*
* SwiftFin is subject to the terms of the Mozilla Public
* License, v2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
import Combine
import Foundation
import JellyfinAPI
final class MovieItemViewModel: ViewModel {
@Published
var item: BaseItemDto
@Published
var isWatched = false
@Published
var isFavorited = false
init(item: BaseItemDto) {
self.item = item
isFavorited = item.userData?.isFavorite ?? false
isWatched = item.userData?.played ?? false
super.init()
}
func updateWatchState() {
guard let id = item.id else { return }
if isWatched {
PlaystateAPI.markUnplayedItem(userId: SessionManager.current.user.user_id!, itemId: id)
.trackActivity(loading)
.sink(receiveCompletion: { [weak self] completion in
self?.HandleAPIRequestCompletion(completion: completion)
}, receiveValue: { [weak self] _ in
self?.isWatched = false
})
.store(in: &cancellables)
} else {
PlaystateAPI.markPlayedItem(userId: SessionManager.current.user.user_id!, itemId: id)
.trackActivity(loading)
.sink(receiveCompletion: { [weak self] completion in
self?.HandleAPIRequestCompletion(completion: completion)
}, receiveValue: { [weak self] _ in
self?.isWatched = true
})
.store(in: &cancellables)
}
}
func updateFavoriteState() {
guard let id = item.id else { return }
if isFavorited {
UserLibraryAPI.unmarkFavoriteItem(userId: SessionManager.current.user.user_id!, itemId: id)
.trackActivity(loading)
.sink(receiveCompletion: { [weak self] completion in
self?.HandleAPIRequestCompletion(completion: completion)
}, receiveValue: { [weak self] _ in
self?.isFavorited = false
})
.store(in: &cancellables)
} else {
UserLibraryAPI.markFavoriteItem(userId: SessionManager.current.user.user_id!, itemId: id)
.trackActivity(loading)
.sink(receiveCompletion: { [weak self] completion in
self?.HandleAPIRequestCompletion(completion: completion)
}, receiveValue: { [weak self] _ in
self?.isFavorited = true
})
.store(in: &cancellables)
}
}
}