add EpisodeItemViewModel

This commit is contained in:
PangMo5 2021-06-19 05:55:50 +09:00
parent 7267b37cb7
commit 4fb792ec24
4 changed files with 139 additions and 106 deletions

View File

@ -125,6 +125,8 @@
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 */; };
62E632E6267D3F5B0063E547 /* EpisodeItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632E5267D3F5B0063E547 /* EpisodeItemViewModel.swift */; };
62E632E7267D3F5B0063E547 /* EpisodeItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632E5267D3F5B0063E547 /* EpisodeItemViewModel.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 */; };
@ -277,6 +279,7 @@
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>"; };
62E632E5267D3F5B0063E547 /* EpisodeItemViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EpisodeItemViewModel.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>"; };
@ -342,6 +345,7 @@
62E632DB267D2E130063E547 /* LibrarySearchViewModel.swift */,
62E632DF267D30CA0063E547 /* LibraryViewModel.swift */,
62E632E2267D3BA60063E547 /* MovieItemViewModel.swift */,
62E632E5267D3F5B0063E547 /* EpisodeItemViewModel.swift */,
);
path = ViewModels;
sourceTree = "<group>";
@ -718,6 +722,7 @@
535870A82669D8AE00D05A09 /* StringExtensions.swift in Sources */,
53ABFDE6267974EF00886593 /* SettingsViewModel.swift in Sources */,
6267B3D826710B9800A7371D /* CollectionExtensions.swift in Sources */,
62E632E7267D3F5B0063E547 /* EpisodeItemViewModel.swift in Sources */,
535870A52669D8AE00D05A09 /* ParallaxHeader.swift in Sources */,
531690F0267ABF72005D8AB9 /* NextUpView.swift in Sources */,
535870A72669D8AE00D05A09 /* MultiSelectorView.swift in Sources */,
@ -768,6 +773,7 @@
53892770263C25230035E14B /* NextUpView.swift in Sources */,
625CB5682678B6FB00530A6E /* SplashView.swift in Sources */,
535BAEA5264A151C005FA86D /* VideoPlayer.swift in Sources */,
62E632E6267D3F5B0063E547 /* EpisodeItemViewModel.swift in Sources */,
5321753B2671BCFC005491E6 /* SettingsViewModel.swift in Sources */,
532175402671EE4F005491E6 /* LibraryFilterView.swift in Sources */,
5377CC01263B596B003A4E83 /* Model.xcdatamodeld in Sources */,

View File

@ -11,62 +11,14 @@ import Combine
struct EpisodeItemView: View {
@StateObject
var tempViewModel = ViewModel()
var viewModel: EpisodeItemViewModel
@State private var orientation = UIDeviceOrientation.unknown
@Environment(\.horizontalSizeClass) var hSizeClass
@Environment(\.verticalSizeClass) var vSizeClass
@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)
}
@ -74,27 +26,27 @@ struct EpisodeItemView: View {
var portraitHeaderOverlayView: some View {
VStack(alignment: .leading) {
HStack(alignment: .bottom, spacing: 12) {
ImageView(src: item.getSeriesPrimaryImage(maxWidth: 120), bh: item.getSeriesPrimaryImageBlurHash())
ImageView(src: viewModel.item.getSeriesPrimaryImage(maxWidth: 120), bh: viewModel.item.getSeriesPrimaryImageBlurHash())
.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)
.fixedSize(horizontal: false, vertical: true)
.offset(y: 5)
HStack {
Text(String(item.productionYear ?? 0)).font(.subheadline)
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)
@ -109,11 +61,11 @@ struct EpisodeItemView: 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))
}
@ -125,19 +77,21 @@ struct EpisodeItemView: 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 {
@ -145,6 +99,7 @@ struct EpisodeItemView: View {
.font(.system(size: 20))
}
}
.disabled(viewModel.isLoading)
}
}.padding(.top, 8)
}
@ -160,19 +115,19 @@ struct EpisodeItemView: 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 ?? "")
}) {
@ -182,13 +137,13 @@ struct EpisodeItemView: 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 ?? "")
@ -213,11 +168,11 @@ struct EpisodeItemView: 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 ?? "")
}) {
@ -233,7 +188,7 @@ struct EpisodeItemView: 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)
@ -241,16 +196,16 @@ struct EpisodeItemView: View {
.blur(radius: 4)
HStack {
VStack {
ImageView(src: item.getSeriesPrimaryImage(maxWidth: 120), bh: item.getSeriesPrimaryImageBlurHash())
ImageView(src: viewModel.item.getSeriesPrimaryImage(maxWidth: 120), bh: viewModel.item.getSeriesPrimaryImageBlurHash())
.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))
}
@ -265,23 +220,23 @@ struct EpisodeItemView: 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 {
Text(String(item.productionYear ?? 0)).font(.subheadline)
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)
@ -289,10 +244,10 @@ struct EpisodeItemView: 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)
@ -306,20 +261,21 @@ struct EpisodeItemView: 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 {
@ -327,21 +283,22 @@ struct EpisodeItemView: 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 ?? "")
}) {
@ -353,13 +310,13 @@ struct EpisodeItemView: 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 ?? "")
@ -384,11 +341,11 @@ struct EpisodeItemView: 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 ?? "")
}) {
@ -409,15 +366,10 @@ struct EpisodeItemView: View {
}
}
}
.onAppear(perform: {
favorite = item.userData?.isFavorite ?? false
watched = item.userData?.played ?? false
settingState = false
})
.onRotate(perform: { orientation in
self.orientation = orientation
})
.navigationBarTitleDisplayMode(.inline)
.navigationTitle("\(item.seriesName ?? "") - S\(String(item.parentIndexNumber ?? 0)):E\(String(item.indexNumber ?? 0))")
.navigationTitle("\(viewModel.item.seriesName ?? "") - S\(String(viewModel.item.parentIndexNumber ?? 0)):E\(String(viewModel.item.indexNumber ?? 0))")
}
}

View File

@ -45,7 +45,7 @@ struct ItemView: View {
} else if item.type == "Series" {
SeriesItemView(item: item)
} else if item.type == "Episode" {
EpisodeItemView(item: item)
EpisodeItemView(viewModel: .init(item: item))
} else {
Text("Type: \(item.type ?? "") not implemented yet :(")
}

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 EpisodeItemViewModel: 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)
}
}
}